Using REST APIs with Protobuf in a Next.js and TypeScript App (Backend in Go)

Compiling .proto Files into TypeScript Using ts-proto

In my experience working with an Nx monorepo Next.js project on Windows, I encountered a path issue that prevented me from compiling the .proto file directly. To resolve this problem, I had to create a separate folder to compile the .proto file and then move the generated TypeScript file back into my monorepo.

protoc --plugin=protoc-gen-ts_proto=.\node_modules\.bin\protoc-gen-ts_proto.cmd --ts_proto_out=. ./simple.proto --ts_proto_opt=esModuleInterop=true

Making API Calls with the KY Library

  1. When working with Protobuf, you may need to add a specific content type to your request headers. In most cases, you would use 'application/protobuf'. However, in my particular case, I had to use 'application/x-protobuf' instead, as 'application/protobuf' was not working. Make sure to test both content types to see which one works for your project.

  2. While working with Protobuf, I initially tried using Axios and set the responseType to 'arraybuffer'. However, I noticed that Axios only returned a partial ArrayBuffer, unlike Fetch, which returned the complete ArrayBuffer. For example, I would get ArrayBuffer(522) with Fetch, but only ArrayBuffer(42) with Axios. Due to this limitation and the fact that KY offers more functionality than Fetch, I decided to switch to using the KY library for handling these requests in my project.

import ky from 'ky';

enum RequestMethod {
  GET = 'get',
  POST = 'post',
  PUT = 'put',
  DELETE = 'delete',
}

interface RequestOptions {
  requestData?: {
    finish: () => Uint8Array;
  };
  headers: Record<string, string>;
}

const apiClient = ky.create({
  prefixUrl: 'http://localhost:4200/api',
  headers: {
    'Content-Type': 'application/x-protobuf',
    Accept: 'application/x-protobuf',
  },
});

async function sendRequest(
  requestPath: string,
  method: RequestMethod,
  options: RequestOptions
) {
  try {
    const serializedData = options.requestData?.finish();
    const response = await apiClient[method](requestPath, {
      body: method === RequestMethod.GET ? undefined : serializedData,
      headers: options.headers,
    }).arrayBuffer(); // somehow axios not work 

    console.log('ky', response);
    return new Uint8Array(response);
  } catch (error) {
    throw new Error(`${requestPath} request failed with status ${error}`);
  }
}

export { RequestMethod, sendRequest };

Encoding Requests and Decoding Responses with Generated TypeScript Files

An example of how to send a registration request using the API.

import * as userProto from 'xxx/proto/user';
import {
  RequestMethod,
  sendRequest,
} from 'xxx';

export async function registerUser({
  email,
  name,
  password,
  verifyCode,
}: userProto.RegisterRequest): Promise<userProto.RegisterResponse> {
  const responseData = await sendRequest(
    'account/register',
    RequestMethod.POST,
    {
      requestData: userProto.RegisterRequest.encode({
        email,
        name,
        password,
        verifyCode,
      }),
      headers: { 'Recaptcha-Token': verifyCode },
    }
  );

  return userProto.RegisterResponse.decode(responseData);
}