DEV Community

Atlas Whoff
Atlas Whoff

Posted on

tRPC vs REST vs GraphQL: Choosing the Right API Layer in 2025

The API Layer Decision

Every full-stack app needs a way for the frontend to talk to the backend. Three options dominate:

  • REST: The default. Works everywhere.
  • GraphQL: Flexible queries. Complex setup.
  • tRPC: Type-safe. TypeScript-only.

Here's when each makes sense.

REST: The Universal Default

// Express REST API
app.get('/api/users/:id', async (req, res) => {
  const user = await db.users.findUnique({ where: { id: req.params.id } });
  if (!user) return res.status(404).json({ error: 'Not found' });
  res.json(user);
});

app.post('/api/users', async (req, res) => {
  const user = await db.users.create({ data: req.body });
  res.status(201).json(user);
});
Enter fullscreen mode Exit fullscreen mode
// Client
const user = await fetch('/api/users/123').then(r => r.json());
// Type: any — no safety
Enter fullscreen mode Exit fullscreen mode

Strengths:

  • Works with any language/client
  • Cacheable (GET requests)
  • Widely understood
  • Great tooling (Swagger, Postman)

Weaknesses:

  • Manual type sync between server and client
  • Over/under-fetching
  • Boilerplate for validation, error handling

Choose REST when: Building a public API, mobile clients, or integrating with non-TypeScript consumers.

GraphQL: Flexible but Heavy

// Schema definition
const typeDefs = gql`
  type User {
    id: ID!
    email: String!
    posts: [Post!]!
  }

  type Query {
    user(id: ID!): User
    users: [User!]!
  }
`;

// Resolver
const resolvers = {
  Query: {
    user: (_, { id }) => db.users.findUnique({ where: { id } }),
  },
  User: {
    posts: (user) => db.posts.findMany({ where: { userId: user.id } }),
  },
};
Enter fullscreen mode Exit fullscreen mode
// Client — fetch only what you need
const { data } = useQuery(gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      email
      posts { title }
    }
  }
`, { variables: { id } });
Enter fullscreen mode Exit fullscreen mode

Strengths:

  • Client controls what data it gets
  • Single endpoint
  • Self-documenting schema
  • Great for complex, nested data

Weaknesses:

  • N+1 problem (need DataLoader)
  • Complex caching
  • High learning curve
  • Overkill for simple CRUD

Choose GraphQL when: Multiple clients with different data needs, complex relationships, public API with diverse consumers.

tRPC: Type-Safe, No Ceremony

// Server — define procedures
import { router, publicProcedure } from './trpc';
import { z } from 'zod';

export const userRouter = router({
  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      return db.users.findUnique({ where: { id: input.id } });
    }),

  create: publicProcedure
    .input(z.object({ email: z.string().email(), name: z.string() }))
    .mutation(async ({ input }) => {
      return db.users.create({ data: input });
    }),
});

export type AppRouter = typeof appRouter;
Enter fullscreen mode Exit fullscreen mode
// Client — fully typed, no code generation
import { trpc } from '../utils/trpc';

function UserPage({ id }: { id: string }) {
  const { data: user } = trpc.user.getById.useQuery({ id });
  // user is typed as User | null — TypeScript knows the shape

  const createUser = trpc.user.create.useMutation();

  return (
    <div>
      <p>{user?.email}</p>
      <button onClick={() => createUser.mutate({ email: 'new@test.com', name: 'New' })}>
        Create
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Strengths:

  • End-to-end type safety — change server type, client errors immediately
  • No code generation step
  • Input validation with Zod built in
  • React Query integration out of the box
  • Minimal boilerplate

Weaknesses:

  • TypeScript only (server + client)
  • No public API use case
  • Not cacheable by CDN (uses POST)

Choose tRPC when: TypeScript monorepo, full-stack Next.js app, internal APIs, small team moving fast.

Decision Matrix

Scenario Recommendation
Public API with docs REST
Mobile + web clients REST or GraphQL
Next.js + TypeScript fullstack tRPC
Complex nested data, multiple clients GraphQL
Simple CRUD, TypeScript tRPC
Microservices REST
Rapid prototyping tRPC

Can You Mix Them?

Yes. Common pattern:

Public API (third-party consumers) → REST
Internal web app → tRPC
Mobile app with complex data → GraphQL
Enter fullscreen mode Exit fullscreen mode

You don't have to pick one globally. Pick the right tool for each consumer.

The worst choice is using GraphQL for a simple CRUD app or REST for a TypeScript monorepo where tRPC would eliminate 80% of the type boilerplate.


tRPC pre-configured with Zod validation, Next.js integration, and React Query: Whoff Agents AI SaaS Starter Kit.

Top comments (0)