What is tRPC? Building Robust APIs with TypeScript and tRPC

Relia Software

Relia Software

Thuoc Nguyen

Relia Software

featured

tRPC (TypeScript Remote Procedure Call) is a framework that simplifies communication between client and server by enabling the definition of typesafe APIs.

Building Robust APIs with TypeScript and tRPC

Table of Contents

In modern web development, ensuring smooth and efficient communication between client and server is crucial. Traditional methods often involve complex setups, multiple layers of serialization and deserialization, and potential mismatches between client and server code. 

tRPC stands out as a powerful solution to these challenges by providing typesafe APIs without the need for schemas. By leveraging TypeScript, tRPC allows developers to define APIs with end-to-end type safety, eliminating the need for separate schema definitions and reducing redundancy and potential errors. This innovative approach simplifies the development process, ensuring that both client and server stay in sync effortlessly. Let's delve deeper into the world of tRPC and explore how it simplifies communication and development.

>> Read more: gRPC vs GraphQL: Choosing the Right API Technology

What is tRPC?

tRPC (TypeScript Remote Procedure Call) is a framework that simplifies client-server communication by enabling the definition of typesafe APIs. This eliminates the need for separate schema definitions and reduces redundancy. Using tRPC offers several advantages over traditional RPC methods:

  • Improved Developer Experience: Streamlines development with type safety, reducing errors and speeding up the process.
  • Enhanced Performance: Potentially improves application performance due to reduced overhead and efficient communication.
  • Strong Typing: Provides robust error handling and ensures data consistency between client and server.

Core Features of tRPC

tRPC offers several core features that make it an attractive choice for modern web development:

Full TypeScript Support

RPC is designed with TypeScript at its core, providing comprehensive support for type definitions across both client and server. This ensures that all parts of the application remain typesafe, significantly reducing the likelihood of type-related bugs and enhancing code maintainability. 

Simplified Data Fetching

By abstracting away much of the boilerplate associated with data fetching, tRPC makes it easier for developers to retrieve and manipulate data. This simplification not only speeds up development but also ensures that data fetching is consistent and reliable across the entire application.

Automatic Type Inference

One of the standout features of tRPC is its ability to automatically infer types. This means that developers do not need to manually define types, as tRPC intelligently deduces them from the context. This not only saves time but also enhances accuracy, ensuring that type definitions are always up-to-date and in sync with the actual data structures used in the application.

Adapters for Popular Frameworks

Adapters in tRPC play a crucial role in integrating tRPC into various frameworks seamlessly. They act as a bridge between tRPC and the specific framework, ensuring that the tRPC router can be plugged into the framework's request-handling pipeline. This allows developers to leverage the benefits of tRPC without having to overhaul their existing project structure.

Next.js Adapter

The Next.js adapter allows tRPC to be easily integrated with Next.js - a popular React framework for building server-rendered applications. The adapter makes it straightforward to set up tRPC endpoints within the Next.js API routes, ensuring smooth and typesafe communication between client and server.

// pages/api/trpc/[trpc].ts
import * as trpcNext from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/router';

export default trpcNext.createNextApiHandler({
  router: appRouter,
  createContext: () => null,
});

Express Adapter

Express is a popular Node.js web framework known for its flexibility and ease of use. The Express adapter simplifies integrating tRPC with existing Express applications. It allows developers to define tRPC endpoints and plug them seamlessly into the Express request-handling pipeline.

// server.ts
import * as trpcExpress from '@trpc/server/adapters/express';
import express from 'express';
import { appRouter } from './router';

const app = express();
app.use('/trpc', trpcExpress.createExpressMiddleware({
  router: appRouter,
  createContext: () => null,
}));

app.listen(4000, () => {
  console.log('Server listening on port 4000');
});

Fastify Adapter

Fastify is another popular Node.js framework known for its high performance and focus on plugins. The Fastify adapter makes it easy to integrate tRPC with Fastify projects. Developers can leverage Fastify's plugin system to effortlessly add tRPC functionality to their applications.

// server.ts
import * as trpcFastify from '@trpc/server/adapters/fastify';
import Fastify from 'fastify';
import { appRouter } from './router';

const fastify = Fastify();

fastify.register(trpcFastify.plugin, {
  prefix: '/trpc',
  trpcOptions: { router: appRouter, createContext: () => null },
});

fastify.listen(4000, (err) => {
  if (err) throw err;
  console.log('Server listening on port 4000');
});

These adapters simplify the integration of tRPC with existing projects by providing out-of-the-box support for popular frameworks. This means developers can leverage tRPC’s typesafe, schema-free API capabilities without having to significantly alter their existing codebase. By using the appropriate adapter, developers can quickly and easily add tRPC to their projects, ensuring a smooth and efficient development process with minimal setup and configuration.

This approach not only accelerates the integration process but also ensures that developers can continue to use the tools and frameworks they are already familiar with, thereby enhancing productivity and maintaining consistency across the project.

Advanced Features for Robust APIs

Middleware

Middleware in tRPC functions similarly to middleware in other web frameworks, acting as a series of processing steps that occur before a request reaches the final procedure or handler. Middleware can be used to add functionality such as authentication, logging, or request transformation. Middleware in tRPC is defined at the router level and can be composed to create complex request handling logic.

We can use Middleware for various purposes:

  • Authentication:

Middleware can be used to ensure that only authenticated users can access certain procedures. By checking the request's authentication token and validating it, middleware can either allow the request to proceed or return an error if the user is not authenticated.

import * as trpc from '@trpc/server';

const isAuthenticated = trpc.middleware(async ({ ctx, next }) => {
  if (!ctx.user) {
    throw new trpc.TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next();
});

const appRouter = trpc.router()
  .middleware(isAuthenticated)
  .query('getUser', {
    resolve: ({ ctx }) => {
      return ctx.user;
    },
  });
  • Request Transformation:

Middleware can modify the request or context before it reaches the procedure. For example, it can add default values, sanitize inputs, or enrich the context with additional data.

const addDefaults = trpc.middleware(async ({ ctx, next }) => {
  ctx.default = 'default value';
  return next();
});

const appRouter = trpc.router()
  .middleware(addDefaults)
  .query('getDefault', {
    resolve: ({ ctx }) => {
      return ctx.default;
    },
  });

Data Validation with Zod

tRPC leverages the power of TypeScript and Zod (a schema declaration and validation library) to simplify data validation on both the server and the client. By using Zod schemas, developers can define the shape and constraints of the data, ensuring that only valid data is processed by the server. This validation occurs seamlessly as part of the tRPC procedure definitions, providing a consistent and typesafe approach to handling data.

  • Server-Side Validation:

On the server side, tRPC uses Zod schemas to validate the input of each procedure. This ensures that incoming data conforms to the expected format before the procedure logic is executed.

import * as trpc from '@trpc/server';
import { z } from 'zod';

// Define a Zod schema for input validation
const userInputSchema = z.object({
  name: z.string().min(1, 'Name is required'),
  age: z.number().min(0, 'Age must be a positive number'),
});

const appRouter = trpc.router()
  .query('createUser', {
    input: userInputSchema,
    resolve: ({ input }) => {
      // Input is guaranteed to be valid at this point
      return `User ${input.name} created, age ${input.age}`;
    },
  });

export type AppRouter = typeof appRouter;
  • Client-Side Validation:

Zod can also be used for client-side validation, providing immediate feedback to users and preventing invalid data from being sent to the server. Here's an example:

import { createTRPCClient, TRPCProvider, trpc } from '@trpc/react';
import { httpBatchLink } from '@trpc/client';
import { z } from 'zod';
import type { AppRouter } from './server';

// Define the client
const client = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:4000/trpc',
    }),
  ],
});

// Define a Zod schema for the input
const userInputSchema = z.object({
  name: z.string().min(1, 'Name is required'),
  age: z.number().min(0, 'Age must be a positive number'),
});

const App = () => {
  const { mutate, error } = trpc.useMutation('createUser');

  const handleSubmit = (input: { name: string; age: number }) => {
    const parsedInput = userInputSchema.safeParse(input);
    if (!parsedInput.success) {
      console.error('Validation failed:', parsedInput.error.errors);
      return;
    }
    mutate(parsedInput.data);
  };

  return (
    <div>
      {error && <p>Error: {error.message}</p>}
      <form
        onSubmit={(e) => {
          e.preventDefault();
          const formData = new FormData(e.currentTarget);
          const name = formData.get('name') as string;
          const age = Number(formData.get('age'));
          handleSubmit({ name, age });
        }}
      >
        <div>
          <label htmlFor="name">Name:</label>
          <input type="text" id="name" name="name" required />
        </div>
        <div>
          <label htmlFor="age">Age:</label>
          <input type="number" id="age" name="age" required min="0" />
        </div>
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

ReactDOM.render(
  <TRPCProvider client={client}>
    <App />
  </TRPCProvider>,
  document.getElementById('root')
);

These features make tRPC and Zod a powerful combination for ensuring robust data validation across both server and client, enhancing the reliability and security of applications.

tRPC vs. The Established Players: A Balanced Comparison

REST and GraphQL APIs

REST (Representational State Transfer) and GraphQL have long been the dominant paradigms in API development.

  • REST APIs: Utilize standard HTTP methods (GET, POST, PUT, DELETE) and endpoints to manage resources. They are relatively simple to understand and implement but often require extensive boilerplate code for defining routes and handling serialization/deserialization.
  • GraphQL APIs: Provide a more flexible approach, allowing clients to query exactly the data they need with a single endpoint. This flexibility comes at the cost of increased complexity, as schema definitions and resolvers are required. Both REST and GraphQL have their strengths and weaknesses, and each has a strong following among developers.

tRPC: A Compelling Alternative

tRPC offers a compelling alternative to REST and GraphQL by focusing on reducing boilerplate code and enhancing type safety. Unlike REST, which often requires extensive boilerplate for defining routes and handling serialization, or GraphQL, which requires schema definitions and resolvers, tRPC leverages TypeScript to define APIs with end-to-end type safety. This approach not only reduces the amount of code developers need to write but also minimizes the risk of runtime errors, leading to a more efficient and reliable development process.

tRPC vs. REST vs. GraphQL

  • Language Support: While REST and GraphQL are language-agnostic, tRPC is designed specifically for TypeScript, providing deep integration and type safety benefits.
  • Performance: REST APIs can suffer from over-fetching or under-fetching data, while GraphQL addresses this with precise queries but can be complex to optimize. tRPC, with its tight integration, can offer performant, typesafe endpoints without the overhead of separate schema management.
  • Developer Experience: REST and GraphQL have extensive ecosystems and tooling. tRPC offers a more streamlined experience for TypeScript developers, reducing boilerplate and leveraging TypeScript's type system for a smoother development process.
  • Flexibility: GraphQL excels in flexibility, allowing clients to specify exactly what data they need. REST is simpler but less flexible. tRPC strikes a balance by providing flexibility with typesafe, declarative API definitions.

Look through the comparison table here:

Feature/Aspect
REST
GraphQL
tRPC
Language Support
Language-agnostic
Language-agnostic
TypeScript-specific
Type Safety
Manual enforcement via typings
Schema-defined
End-to-end type safety
Boilerplate
High
Medium to high (schema definitions)
Low (leverages TypeScript)
Performance
Varies (over-fetching/under-fetching)
Efficient queries, complex optimizations
Efficient, minimal overhead
Developer Experience
Mature tooling and ecosystem
Rich ecosystem, steep learning curve
Streamlined for TypeScript developers
Flexibility
Moderate (fixed endpoints)
High (flexible queries)
Moderate to high (flexible, typesafe)
Tooling
Extensive (Swagger, Postman, etc.)
Extensive (Apollo, GraphiQL, etc.)
Growing (integrates with TypeScript)
Adoption
Widely adopted
Increasing adoption
Emerging
Learning Curve
Moderate
Steep
Low to moderate

>> Read more about TypeScript:

Conclusion

tRPC offers a typesafe, minimal-boilerplate alternative to REST and GraphQL, particularly well-suited for TypeScript developers. It combines the simplicity of REST with the type safety and some of the flexibility of GraphQL, making it an attractive option for modern web development where TypeScript is heavily used.

While REST and GraphQL have well-established ecosystems and broad language support, tRPC's seamless integration with TypeScript and focus on developer experience present a compelling case for its adoption in projects where these factors are prioritized.

Ready to explore tRPC? Check out the official documentation (https://trpc.io/docs/quickstart) to learn more about its features and implementation details.

>>> Follow and Contact Relia Software for more information!

  • development
  • coding