API Design in 2026: REST, GraphQL, tRPC, or Something Else?
Back to Blog

API Design in 2026: REST, GraphQL, tRPC, or Something Else?

March 21, 20262 min read2 views

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 string

Use 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 tRPC

Universal 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.

Share this article