The API design landscape has shifted significantly. GraphQL adoption has plateaued after years of hype, tRPC is gaining serious momentum in the TypeScript ecosystem, and REST remains the sensible default for public APIs.
REST in 2026: Still the Default
REST wins for public APIs. It's universally understood, cacheable, and works with any client.
// Modern REST with OpenAPI typing
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { z } from 'zod'
const getUserRoute = createRoute({
method: 'get',
path: '/users/{id}',
request: {
params: z.object({ id: z.string().uuid() })
},
responses: {
200: {
content: { 'application/json': { schema: UserSchema } },
description: 'User found'
},
404: { description: 'User not found' }
}
})Use REST when: Public API, multiple client platforms, caching matters, team familiarity.
GraphQL: Where It Shines
GraphQL excels when clients have diverse, unpredictable data needs.
type Query {
user(id: ID!): User
users(filter: UserFilter): [User!]!
}
type User {
id: ID!
name: String!
posts(first: Int, after: String): PostConnection!
followers: [User!]!
}Use GraphQL when: Mobile apps with bandwidth constraints, complex data relationships, multiple frontend teams with different needs.
Avoid GraphQL when: Simple CRUD, server-to-server, team lacks GraphQL experience.
tRPC: Full-Stack TypeScript Dream
tRPC eliminates the API layer for TypeScript full-stack apps.
// server/routers/user.ts
export const userRouter = router({
getById: publicProcedure
.input(z.object({ id: z.string().uuid() }))
.query(async ({ input }) => {
return db.query.users.findFirst({ where: eq(users.id, input.id) })
}),
create: publicProcedure
.input(CreateUserSchema)
.mutation(async ({ input }) => {
return db.insert(users).values(input).returning()
})
})
// client - fully typed, zero codegen!
const user = await trpc.user.getById.query({ id: '123' })
user.name // TypeScript knows this is stringUse tRPC when: TypeScript frontend and backend, same team owns both, internal APIs.
gRPC and Connect
For high-performance service-to-service communication:
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User);
}
message User {
string id = 1;
string name = 2;
string email = 3;
}Use gRPC when: Microservices, performance critical, streaming needed.
Decision Framework
Public API → REST with OpenAPI
GraphQL experience + complex queries → GraphQL
TypeScript full-stack, same team → tRPC
Microservices, high performance → gRPC
Simple internal API → REST or tRPCUniversal API Design Principles
Regardless of protocol:
- Consistent naming conventions
- Clear error responses with actionable messages
- Versioning strategy from day one
- Rate limiting and authentication
- Documentation that stays in sync (OpenAPI, GraphQL introspection, tRPC inference)
Conclusion
There's no one-size-fits-all answer. Understand the trade-offs, consider your team's expertise, and choose based on your actual requirements—not trends.
