mdashikjs
← Back to blog
Backend3 min

Building a Type-Safe API Layer with tRPC and Next.js

I have shipped REST, GraphQL, and now tRPC in production. For an app where one team owns both the frontend and the backend, tRPC is the most productive by a wide margin.

tRPCNext.jsTypeScriptAPI Design

I have shipped REST, GraphQL, and now tRPC in production. For an internal app with one frontend and one backend, tRPC is the most productive of the three by a wide margin.

Why not GraphQL

GraphQL solves a problem I did not have: multiple consumers with different shape requirements. My frontend and backend are the same deploy, maintained by the same people. GraphQL's type-safety-via-codegen pays off when you own the schema but not the consumers. When you own both, tRPC's direct TypeScript inference is faster and has fewer moving parts.

Router structure

I keep routers tight and domain-aligned. One router per bounded context:

// server/routers/_app.ts
import { router } from '../trpc';
import { postsRouter } from './posts';
import { usersRouter } from './users';

export const appRouter = router({
  posts: postsRouter,
  users: usersRouter,
});

export type AppRouter = typeof appRouter;

That AppRouter type is the whole magic. It is imported on the client as a type-only import, so there is zero runtime cost for the type safety you get on every call site.

Input validation with Zod

Every procedure gets a Zod schema. I do not trust client input, and I do not trust myself to remember that at 11pm.

const createPost = publicProcedure
  .input(z.object({
    title: z.string().min(1).max(200),
    body: z.string().min(1),
  }))
  .mutation(async ({ input, ctx }) => {
    return ctx.db.post.create({ data: input });
  });

The one gotcha

Procedures defined as arrow functions on objects lose this context. If you are wrapping a class-based service, bind it explicitly or wrap in a closure. I lost an afternoon to this.

When tRPC is wrong

Public APIs. Anything a third party will consume. Anything non-TypeScript. For those, give me a boring REST contract and OpenAPI docs.