7 Essential Steps to Building Scalable, Production-Ready tRPC APIs with TypeScript

When it comes to building scalable and production-ready APIs, developers often turn to established frameworks like GraphQL Federation. However, as any seasoned developer can attest, these tools can come with their own set of challenges and complexities. In this article, we’ll delve into the world of tRPC, a revolutionary new approach to building APIs that’s gaining traction in the industry.

building trpc apis

The Problem with Traditional API Architectures

Traditional API architectures, such as those built using GraphQL Federation, can be cumbersome and difficult to maintain. One of the primary concerns is schema synchronization, which can become a nightmare as the number of services and dependencies grows. In a typical GraphQL Federation setup, schema changes require a convoluted process involving code generation, resolver updates, and client-side code regeneration. This not only leads to increased complexity but also introduces the risk of schema-related issues that can manifest in production.

Introducing tRPC: A New Paradigm for API Development

tRPC (tClient) is an innovative approach to building APIs that eliminates the need for schema definitions, thereby reducing the risk of API bugs and improving overall performance. By leveraging TypeScript, tRPC enables developers to create robust and scalable APIs that are free from the schema-related issues plaguing traditional architectures. In this article, we’ll explore the essential steps required to build production-ready tRPC APIs with TypeScript.

Step 1: Setting Up the Development Environment

Before diving into the world of tRPC, it’s essential to set up a development environment that’s conducive to building scalable and production-ready APIs. This involves installing the necessary dependencies, including the tRPC library, and configuring the project structure. To get started, create a new Next.js 14 project using the following command:

npx create-next-app my-app --ts

Step 1.1: Installing tRPC and Related Dependencies

Next, install the tRPC library and related dependencies using npm or yarn:

npm install @trpc/client @trpc/server

Step 1.2: Configuring the Project Structure

Configure the project structure by creating a new folder for the API and adding the necessary files, including the `trpc.ts` file that will serve as the central hub for API definitions.

Step 2: Defining API Endpoints with tRPC

With the development environment set up, it’s time to define API endpoints using tRPC. This involves creating a new file, `api.ts`, where you’ll define the API endpoints using the `createTRPCClient` function.

Step 2.1: Defining API Endpoints

Define API endpoints using the `createTRPCClient` function, which takes in the `api` instance and returns a client object that can be used to make API calls:

import { createTRPCClient } from '@trpc/client';
import { httpLink } from '@trpc/client';
import { createNextApiHandler } from 'next';
import { trpc } from './trpc';

export const appRouter = trpc.router().query('get-users', {
 async resolve() {
 return [
 { id: 1, name: 'John Doe' },
 { id: 2, name: 'Jane Doe' },
 ];
 },
 }).mutation('create-user', {
 input: z.object({
 id: z.string(),
 name: z.string(),
 }),
 async resolve({ input }) {
 // Create a new user
 return { id: input.id, name: input.name };
 },
 });

export default createNextApiHandler({
 router: appRouter,
 // Add middlewares or configuration options as needed
});

Step 3: Implementing API Routing with Next.js 14 App Router

With the API endpoints defined, it’s time to implement API routing using Next.js 14 App Router. This involves creating a new file, `api/route.ts`, where you’ll define the API routes using the `appRouter` instance.

You may also enjoy reading: Google Gemini Finally Runs on a Single Air-Gapped Server, Then Disappears Forever.

Step 3.1: Defining API Routes

Define API routes using the `appRouter` instance, which provides a simple and declarative way to define API endpoints:

import { appRouter } from './api';

export default appRouter.createRoute({
 method: 'GET',
 path: '/users',
 }).handler(async () => {
 const users = await appRouter.query('get-users')();
 return users;
 });

export default appRouter.createRoute({
 method: 'POST',
 path: '/users',
 }).handler(async ({ req }) => {
 const userInput = req.body;
 const user = await appRouter.mutation('create-user')({ input: userInput });
 return user;
 });

Step 4: Handling API Errors and Logging

When building production-ready APIs, it’s essential to handle API errors and logging. This involves implementing error-handling middleware and logging mechanisms to ensure that errors are properly handled and logged.

Step 4.1: Implementing Error-Handling Middleware

Implement error-handling middleware using Next.js 14 App Router’s built-in `onError` function:

import { NextApiRequest, NextApiResponse } from 'next';
import { appRouter } from './api';

appRouter.createRoute({
 method: 'GET',
 path: '/users',
 }).handler(async (req: NextApiRequest, res: NextApiResponse) => {
 try {
 const users = await appRouter.query('get-users')();
 return users;
 } catch (error) {
 return res.status(500).json({ message: 'Internal Server Error' });
 }
 }).onError((error) => {
 // Log the error using a logging library like Winston
 console.error(error);
 // Return a default error response
 return res.status(500).json({ message: 'Internal Server Error' });
 });

Step 5: Optimizing API Performance with Caching and Code Splitting

Optimizing API performance is crucial for building scalable and production-ready APIs. This involves implementing caching and code splitting to reduce API call latency and improve overall performance.

Step 5.1: Implementing Caching

Implement caching using a library like Redis or a caching layer like Varnish Cache:

import { appRouter } from './api';

appRouter.createRoute({
 method: 'GET',
 path: '/users',
 }).handler(async (req, res) => {
 // Check if the user is cached
 const cachedUser = await Redis.get(`user:${req.query.userId}`);
 if (cachedUser) {
 return cachedUser;
 }
 // Fetch the user from the database
 const user = await db.query('SELECT * FROM users WHERE id =?', [req.query.userId]);
 // Cache the user for 1 hour
 Redis.set(`user:${req.query.userId}`, user, 'EX', 3600);
 return user;
 });

Step 5.2: Implementing Code Splitting

Implement code splitting using Next.js 14 App Router’s built-in `getStaticProps` function:

import { NextApiRequest, NextApiResponse } from 'next';
import { appRouter } from './api';

export async function getStaticProps() {
 // Fetch the users from the database
 const users = await db.query('SELECT * FROM users');
 // Return the users as static props
 return {
 props: {
 users,
 },
 };
}

appRouter.createRoute({
 method: 'GET',
 path: '/users',
 }).handler(async (req: NextApiRequest, res: NextApiResponse) => {
 // Return the static props
 return res.json({ users });
 });

Add Comment