Type-Safe API Communication with tRPC
Published: 2024-01-24 23:58:06

In the ever-evolving landscape of web development, writing code that is not only efficient but also easy to maintain is a constant endeavor. One of the key aspects that developers grapple with is ensuring type safety, especially when it comes to the communication between a client and a server. In this blog post, we'll explore how tRPC (Typed RPC) emerges as a powerful tool to address this challenge, providing a seamless blend of familiarity and robust type safety.

## The Challenge of Traditional API Communication
Traditional REST-based API communication, while widely adopted, often falls short in maintaining strong type safety. Developers face uncertainties when it comes to changes in route names, data structures, or response formats. The lack of immediate feedback can lead to runtime errors, making the debugging process more cumbersome.

## Enter tRPC: Bridging the Gap Between REST and GraphQL
tRPC steps in as a bridge between traditional REST practices and the advanced type safety of GraphQL. It offers a fresh perspective on API communication, combining the best of both worlds.

## Setting the Stage with a REST Example
Let's start by examining a scenario where a developer is working on a project using standard REST practices. An Express server is established, handling various routes.

```
import express from "express"
const router = express.Router()

router.get<{ names: string }>("/greetings", (req, res) => {
  res.send(`Hello ${req.query.names}`)
})

router.get<{ name: string }>("/error", (req, res) => {
  res.status(500).send("This is an error message")
})

export default router
```
In this scenario, a developer making changes to route names or data structures may face challenges in ensuring that the client-side code remains consistent with the server-side implementation. Type safety is not guaranteed, and errors might only surface at runtime. For example, if you change the endpoint to "/greetings" instead of "/greeting" and then attempt to request the "/greeting" endpoint from the client-side, you won't immediately see an error until you run the code. This is because the TypeScript types for the endpoint names are defined in the route handlers, and there is no compile-time checking to catch these mistakes.

When you run the code and make a request to "/greeting" (assuming the endpoint is defined as "/greetings"), you will likely encounter a runtime error, possibly resulting in a 404 Not Found response or some other unexpected behavior.

This is in contrast to using tRPC or a similar framework where the TypeScript types are generated based on your API definition. In such frameworks, if you attempt to make a request to a non-existent or misspelled endpoint, the TypeScript compiler will catch this error during the compilation process, providing early feedback and helping you avoid runtime issues.

## Empowering Development with tRPC
Now, let's transition to a world where tRPC becomes a central player in API communication. The same scenario is reimagined, but this time with tRPC seamlessly integrated.

**Server-Side Integration**
On the server side, tRPC brings about a paradigm shift in route definition and input validation using Zod.

**How to add tRPC to existing Express project**
```
// tRPC Middleware for Express
import { createExpressMiddleware} from "@trpc/server/adapters/express"
import express from 'express'
import cors from "cors"
import {appRouter} from "./routers"

const app=express()
app.use(cors({origin:"http://localhost:5173"}))

app.use('/trpc', createExpressMiddleware({
  router:appRouter,
  createContext:({req,res})=>{ return {} },
}));
app.listen(3000)
```
Key features include:
**tRPC Middleware**: tRPC provides middleware for Express, making it easy to integrate with existing setups.

**Type-Defined Routes**: Routes are now explicitly defined, complete with input validation using Zod, a powerful TypeScript-first schema declaration library.

**Setting up routes**
```
// Importing necessary dependencies
import { t } from "../trpc";  // Assuming 't' is an object with utility functions for defining TRPC routers
import { z } from "zod";  // Assuming 'z' is an object with utility functions for defining schemas
import { usersRouter } from "./users";  // Importing another router from a different module

export const appRouter = t.router({
    greeting: t.procedure
     .input(z.object({ name: z.string() }))
     .query(requestObj => {
      console.log(requestObj);
      return `Hello ${requestObj.input.name}`;
    }),
  
    errors: t.procedure.query(() => {
      throw new Error("This is an error message");
    }),
    users: usersRouter,
});

export type AppRouter = typeof appRouter;

```
Inside the router definition, there are three properties:

greeting: A query that expects an input object with a name property of type string. The query logs the input object and returns a greeting message that includes the provided name.

errors: A query that intentionally throws an error with the message "This is an error message" when executed.

users: This property is assigned the value of another router (usersRouter) imported from a different module.

TypeScript's static type checking ensures that the code adheres to the specified types during development. This helps catch type-related errors at compile time, providing early feedback to developers.

**Client-Side Integration**
On the client side, the tRPC client takes center stage, providing developers with autocompletion, immediate feedback, and most importantly, type safety.

Now, let's say someone on the development team decides to change the name of the greeting query from "greeting" to "welcome":
Now, if the development team attempts to use the greeting query in the codebase after this change, TypeScript's static type checking will catch the error at compile time. 

```
// tRPC Client Setup
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client"
import type { AppRouter } from "../../server/routers"

const client = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: "http://localhost:3000/trpc",
    }),
  ],
})

async function main() {
  const result = await client.greeting.query({ name: "Kyle" })
  console.log(result)
}
```
The code is now more intuitive and developer-friendly. The tRPC client allows developers to call API functions as if they were invoking regular functions. The TypeScript type system ensures that any inconsistencies, such as changes in route names or data structures, are immediately flagged.

## Conclusion
tRPC emerges as a powerful ally in the quest for type-safe API communication. By seamlessly integrating with familiar tools and frameworks, tRPC brings a new level of confidence and efficiency to the development process.

In a world where the complexities of GraphQL meet the simplicity of REST, tRPC stands out as a beacon, offering developers a robust solution without compromising on ease of use. Embrace the power of tRPC and elevate your web development experience with enhanced type safety and streamlined API communication.