tRPC vs REST vs GraphQL: Choosing Your API Layer for Full-Stack TypeScript

The API layer is the contract between your frontend and backend. For full-stack TypeScript teams, three approaches dominate: REST's universal familiarity, GraphQL's precise data fetching, and tRPC's end-to-end type safety. Each imposes different trade-offs on developer experience, type safety guarantees, and runtime performance. Here is how to decide.
End-to-End Type Safety: The tRPC Difference
tRPC eliminates the conceptual gap between backend functions and frontend calls. Your backend procedures become directly callable from the frontend with full TypeScript inference — no code generation, no schema definitions, no duplication.
// Backend: server/router.ts
import { initTRPC } from '@trpc/server'
import { z } from 'zod'
const t = initTRPC.create()
export const appRouter = t.router({
getUser: t.procedure
.input(z.string())
.query(({ input }) => db.user.findUnique({ where: { id: input } })),
createProject: t.procedure
.input(z.object({ name: z.string(), teamId: z.string() }))
.mutation(({ input }) => db.project.create({ data: input })),
})
export type AppRouter = typeof appRouter
// Frontend: client.ts
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from './server/router'
const trpc = createTRPCReact<AppRouter>()
// Fully type-safe — inferred return type, validated input
const user = await trpc.getUser.query('user_123')
const project = await trpc.createProject.mutate({
name: 'My Project',
teamId: 'team_456'
})
REST requires manual type definitions or OpenAPI-to-TypeScript code generation. GraphQL relies on codegen tools like GraphQL Code Generator, which introduce a build step. tRPC gives you type safety with zero tooling overhead.
REST: Universal Compatibility
REST's strength is its universality. Every HTTP client speaks REST natively, caching layers like CDNs understand GET requests, and the mental model of resources and collections is well understood. For public APIs consumed by third parties, REST remains the standard.
// Standard REST client with Zod validation
import { z } from 'zod'
const ProjectSchema = z.object({
id: z.string(),
name: z.string(),
teamId: z.string(),
})
async function getProject(id: string) {
const res = await fetch(`/api/projects/${id}`)
if (!res.ok) throw new Error(`Failed: ${res.status}`)
return ProjectSchema.parse(await res.json())
}
The trade-off is manual serialization boundaries. Each endpoint needs documentation, error handling is ad-hoc, and keeping frontend types in sync with backend schemas requires discipline or code generation.
GraphQL: Precise Data Fetching at Scale
GraphQL shines in data-fetching scenarios where over-fetching and under-fetching are real problems — dashboards, feed-based UIs, and mobile applications with bandwidth constraints.
query GetProjectWithTasks($id: ID!) {
project(id: $id) {
id
name
tasks(first: 10) {
edges {
node {
id
title
status
}
}
}
}
}
GraphQL's declarative data fetching gives clients complete control over responses. The trade-offs are complexity: a schema definition, resolver layer, N+1 query mitigation with DataLoader, and caching strategies that require Apollo Client or Relay.
Performance Considerations
tRPC uses HTTP/2 by default, batching requests without the overhead of a query language parser. REST benefits from HTTP caching at every layer. GraphQL payloads are smaller on the wire for complex queries but require parsing and validation on every request.
A production benchmark querying a list of 50 projects with their first 5 tasks showed:
- REST (3 round trips): 180 ms total
- GraphQL (1 round trip): 95 ms including parsing
- tRPC (1 batch): 85 ms
The differences narrow as cache hit rates increase, but tRPC and GraphQL reduce round trips for complex data shapes.
Choosing Your API Layer
Pick tRPC when you control both frontend and backend, value end-to-end type safety above all else, and want minimal boilerplate. It is ideal for internal tools, SaaS applications, and monorepo projects.
Pick REST when you expose a public API, need universal client compatibility, or leverage HTTP caching extensively. It remains the safest choice for third-party integrations and long-lived API contracts.
Pick GraphQL when you serve multiple client types with different data requirements, need a schema registry for API governance, or operate at a scale where bandwidth optimization matters.
Practical Migration
Teams migrating between approaches typically start with REST, add GraphQL as data-fetching complexity grows, and adopt tRPC on greenfield projects. A hybrid approach works: REST for public endpoints, tRPC for internal consumption, with both sharing the same service layer.
Making the Right Call
Your API layer choice should follow your team's tolerance for tooling complexity and your type safety requirements. tRPC removes an entire class of integration bugs at the cost of coupling frontend and backend. REST provides stability and ubiquity. GraphQL offers flexibility at a complexity premium.
At SoniNow, we architect API layers that match project needs. Our web development services include full-stack TypeScript architecture, from tRPC monorepos to GraphQL federated gateways.
Ship with confidence. Let SoniNow design your API layer for maximum developer velocity and type safety.
Related Insights

API Rate Limiting Strategies: Token Bucket, Leaky Bucket, and Sliding Window
A guide to implementing API rate limiting including token bucket, leaky bucket, sliding window, and distributed rate limiting with Redis for production APIs.

CORS Configuration: Cross-Origin Resource Sharing Done Safely
Learn CORS configuration including allowed origins, methods, headers, credentials, preflight requests, and common security pitfalls when exposing APIs.

Building a Design System with React, TypeScript, and Storybook
A complete guide to building a scalable design system using React, TypeScript, and Storybook including component architecture, theming, accessibility, and documentation.