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

Jason ChenJason Chen
2 min read

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);
}
0
Subscribe to my newsletter

Read articles from Jason Chen directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jason Chen
Jason Chen

Nothing is true, everything is permitted.