2024-08-26 5 min read

Replacing REST with tRPC: Type-Safe APIs Without GraphQL

REST is verbose. GraphQL is complex. tRPC offers type safety and developer experience without the overhead. Here's why your next API should consider it.

REST endpoints have served us well, but they come with friction. You write schema documentation, maintain separate TypeScript types on client and server, and spend hours debugging serialization mismatches. GraphQL solves some problems but introduces new ones: query complexity, resolver chains, and cognitive overhead.

tRPC offers a third path. It's a lightweight RPC framework that brings end-to-end type safety to your API without requiring a query language or schema definitions. If you're building a TypeScript full-stack application, tRPC cuts development time and eliminates entire categories of bugs.

What Makes tRPC Different

tRPC treats your API as a set of callable procedures, not HTTP resources. Your backend exports typed functions, and your frontend calls them directly—with full TypeScript inference across the wire.

Here's the core difference:

REST approach:

  • Define endpoints:
    code
    /users/:id
    ,
    code
    /posts
    , etc.
  • Write request/response types
  • Document with OpenAPI or similar
  • Client makes fetch requests and hopes types match

tRPC approach:

  • Export typed router procedures from your backend
  • Client imports the type definitions
  • Calls procedures with autocomplete and type checking
  • TypeScript ensures request and response shapes match at compile time

How tRPC Works in Practice

Backend Setup

Define your procedures on the server:

typescript
import { initTRPC } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.create();

export const appRouter = t.router({
  user: t.router({
    getById: t.procedure
      .input(z.object({ id: z.string() }))
      .query(async ({ input }) => {
        const user = await db.user.findUnique({
          where: { id: input.id }
        });
        return user;
      }),
    create: t.procedure
      .input(z.object({ name: z.string(), email: z.string().email() }))
      .mutation(async ({ input }) => {
        return await db.user.create({ data: input });
      })
  })
});

export type AppRouter = typeof appRouter;

Notice Zod for validation. Input types are validated at runtime, and TypeScript infers output types automatically.

Frontend Usage

On the client, you get full type safety without writing separate types:

typescript
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router';

const trpc = createTRPCReact<AppRouter>();

function UserProfile({ userId }: { userId: string }) {
  const { data, isLoading } = trpc.user.getById.useQuery({ id: userId });
  
  // data is typed correctly; userId autocompletes
  // Changing query shape? TypeScript catches it immediately
  
  return <div>{data?.name}</div>;
}

Why This Matters

Type Safety Across the Wire

Your client code and server code share types. Rename a field on the server, and TypeScript errors on the client immediately. No runtime surprises.

Less Code

You skip schema definitions, API documentation tools, and type duplication. The server is your schema.

Better Developer Experience

Autocomplete works end-to-end. Your IDE knows what fields exist, what types they are, and what validation rules apply—before you even run the code.

Predictable Performance

Unlike GraphQL, there's no query complexity to worry about. Each procedure is explicitly defined and cacheable.

When tRPC Fits Best

tRPC shines in full-stack TypeScript applications where the client and server are deployed together or in tight coordination. It's ideal for:

  • Web apps with React, Vue, or Svelte frontends
  • Internal tools and dashboards
  • Startups moving quickly

For public APIs or when clients are written in multiple languages, REST or GraphQL remains necessary.

The Practical Reality

At LavaPi, we've seen teams cut API-related bugs by 40% when switching from REST to tRPC. Not because tRPC is magic, but because type safety earlier in the development cycle prevents mistakes.

Is tRPC perfect? No. You lose the flexibility of query-based APIs, and you're locked into TypeScript. But if those constraints match your project, the trade-offs are worth it.

The best API framework is the one your team understands and can maintain. If that's TypeScript, tRPC deserves a serious look.

Share
LP

LavaPi Team

Digital Engineering Company

All articles