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);
});
// Client
const user = await fetch('/api/users/123').then(r => r.json());
// Type: any — no safety
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 } }),
},
};
// Client — fetch only what you need
const { data } = useQuery(gql`
query GetUser($id: ID!) {
user(id: $id) {
email
posts { title }
}
}
`, { variables: { id } });
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;
// 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>
);
}
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
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)