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
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.
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);
}