
APIs underpin most modern software systems. Whether you’re building a SaaS dashboard, a mobile app, or coordinating microservices, how you expose your data shapes your velocity, flexibility, and technical debt.
Through several years of building production systems with React and TypeScript, I’ve shipped REST, GraphQL, and tRPC APIs. Each option presents distinct strengths, with real-world tradeoffs developers and engineering leaders should understand. This guide compares these technologies from a practical engineering perspective, focusing on architecture, type safety, toolchains, and developer experience.
API Approaches Explained
REST: The Web Standard
REST (Representational State Transfer) organizes APIs around resources, linked to URL endpoints (e.g., /users/42). Clients interact using standard HTTP methods (GET, POST, PUT, DELETE). It’s simple, widely supported, and language-agnostic.
GraphQL: Flexible Queries
GraphQL, developed by Facebook, enables clients to query precisely the data they need via a single endpoint, using a structured query language. This model suits dynamic UIs and data aggregation scenarios, minimizing overfetching and underfetching.
tRPC: Type Safety for TypeScript
tRPC provides end-to-end type safety by exposing backend procedures directly to TypeScript clients, without code generation or manual typings. If you work in a full-stack TypeScript environment-especially with Next.js or monorepos-the type inference between client and server can accelerate iteration and reduce bugs.
Core Comparison Table
REST | GraphQL | tRPC | |
Endpoints | Resource URLs | Single endpoint, multiple queries | Procedure calls |
Type Safety | Manual | Optional (schema/codegen) | Automatic, end-to-end (TS only) |
Overfetch Risk | Common | Minimal | Minimal |
Best For | Public APIs, CRUD | Dynamic UIs, aggregation | Full-stack TypeScript, internal APIs |
Language Support | Broad, language-agnostic | Broad, language-agnostic | TypeScript only |
Adoption Patterns
REST
- Works well for simple CRUD services, public APIs, or any system where resource semantics map cleanly to endpoints.
- Typical in e-commerce catalogs, third-party integrations, and services needing broad language support.
GraphQL
- Best for complex, evolving UIs that need flexible querying and combine multiple backend sources.
- Common in product dashboards, social applications, and mobile-first projects.
tRPC
- Suits full-stack TypeScript codebases-especially internal tools, admin panels, or monolithic/monorepo architectures.
- Ideal for teams optimizing for rapid prototyping, consistent types, and minimized boilerplate.
Practical Pros and Cons
REST
Advantages
- Simple; nearly every developer is familiar with the approach.
- Extensive tooling (e.g., Swagger/OpenAPI).
- Easy debugging, request logging, and use of HTTP standards for cache/control.
- Language-agnostic: any HTTP client can consume a REST API.
Limitations
- Clients often overfetch or underfetch data; multiple round-trips needed for complex UI.
- No inherent type contracts; requires extra effort to keep docs accurate.
- Evolving API shape safely over time can be tricky.
GraphQL
Advantages
- Clients retrieve exactly the data they request.
- Introspection and live schema documentation built-in.
- Enables rapid frontend iteration; backward-compatible evolution.
Limitations
- More initial setup and complexity: schema, resolvers, types.
- Caching and monitoring need additional patterns.
- Overly flexible: potential for performance traps like N+1 queries.
tRPC
Advantages
- End-to-end type safety between client and server.
- No code generation or manual type maintenance.
- Fast feedback loop, minimal boilerplate, and strong DX in shared TypeScript projects.
- With Zod, runtime input validation is trivial.
Limitations
- Only works in TypeScript; not suitable for public APIs or polyglot backends.
- Tightly couples front- and backend; not well-suited for external consumers.
Best Practices
REST
- Use clear, hierarchical resource URLs (e.g., /users/42/orders).
- Apply HTTP verbs and status codes consistently.
- Document endpoints with OpenAPI/Swagger.
- Plan for versioning (/api/v1/users), as breaking changes will happen.
GraphQL
- Enforce schemas with linting and validation (e.g., GraphQL Codegen, Apollo Studio).
- Optimize resolvers to address performance (N+1 issues, batching).
- Gate mutations and sensitive queries with auth and access controls.
tRPC
- Keep procedures focused and explicitly typed.
- Validate inputs with Zod or similar schema validation.
- Export router types for client-side type inference.
- Even with strong internal typing, document procedures for onboarding and maintainability.
Real Examples
See this public GitHub repository for code samples illustrating all three API types.
Troubleshooting Tips and Common Pitfalls
REST
- Manage Endpoint Sprawl: Resist the temptation to create many similar endpoints for slight variations of data. Keep your endpoint surface area as small and consistent as possible to ease maintenance.
- API Versioning: Implement versioning (e.g., /v1/users) early and consistently. This avoids breaking existing clients as your API evolves. Regularly audit API usage to detect version drift and outdated clients.
GraphQL
- Query Complexity: Monitor query execution and set limits on depth and complexity. Deeply nested or unbounded queries can cause unexpected server load and performance bottlenecks. Use query cost analysis tools or plugins.
- Restrict Public Queries: Avoid exposing generic “catch-all” queries in public APIs. Limit scope and apply strict access controls to prevent abuse-especially on endpoints that join or aggregate large datasets.
tRPC
- Infrastructure Abstraction: Do not expose backend infrastructure, such as database schema or raw table structures, through procedures. Keep your API surface aligned with domain concepts, not database details.
- Domain-Focused Procedures: Design your API around business logic rather than CRUD operations at the database level. This keeps the contract stable and abstracts away internal changes from clients.
- Internal-Only by Design: tRPC is intended for internal APIs within TypeScript monorepos or full-stack apps. Avoid using tRPC for public APIs or cases involving teams working in multiple languages.
How to Choose
- If you’re building an internal, full-stack TypeScript tool (e.g., with Next.js): tRPC delivers unmatched speed and type safety for TypeScript-first teams. Fewer bugs, near-zero manual typings, and instant feedback during refactorings.
- If your frontend is complex, data requirements are fluid, or you aggregate multiple backend sources: GraphQL’s flexibility is worth the up-front learning curve.
If you’re exposing a public API, supporting multiple languages, or need long-term backward compatibility: REST is stable, battle-tested, and universally supported.