HTTP Conventions vs End-to-End Type Safety
REST is: the established API paradigm. Resources as URLs (GET /api/users/:id), HTTP methods for actions (GET, POST, PUT, DELETE), status codes for outcomes (200, 404, 500), and JSON bodies for data. REST is: language-agnostic (any client, any server), well-tooled (OpenAPI, Swagger, Postman), and universally understood. The trade-off: the client and server have separate type definitions that can drift (the API response shape in the backend may not match the TypeScript type in the frontend).
tRPC is: a TypeScript-specific API framework that eliminates the API layer. Define procedures on the server: const userRouter = router({ getUser: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input }) => { return db.select()... }) }). Call from the client: const user = trpc.user.getUser.useQuery({ id: '123' }). The TypeScript types: flow from server to client automatically. No fetch, no API types, no drift. The trade-off: works only in TypeScript monorepos where client and server share the type system.
Without API paradigm rules: the AI generates REST patterns in a tRPC project (creating /api/users routes when the project uses tRPC routers) or tRPC patterns in a REST project (importing trpc.user.getUser when the project uses fetch). The API choice determines: how every data interaction is structured. One rule prevents: every API pattern mismatch.
Endpoint Design: Resource URLs vs Router Procedures
REST endpoint design: resource-based URLs with HTTP methods. GET /api/users (list), GET /api/users/:id (detail), POST /api/users (create), PUT /api/users/:id (update), DELETE /api/users/:id (delete). Each endpoint: a separate route handler. In Next.js: app/api/users/route.ts (list + create) and app/api/users/[id]/route.ts (detail + update + delete). AI rule: 'REST: resource URLs, HTTP methods for CRUD. Next.js Route Handlers in app/api/. Zod validation in each handler.'
tRPC endpoint design: procedure-based routers. const userRouter = router({ list: publicProcedure.query(...), getById: publicProcedure.input(z.object({ id: z.string() })).query(...), create: publicProcedure.input(createUserSchema).mutation(...), update: publicProcedure.input(updateUserSchema).mutation(...), delete: publicProcedure.input(z.object({ id: z.string() })).mutation(...) }). All procedures: in one router file. In Next.js: one API route (app/api/trpc/[trpc]/route.ts) handles all procedures. AI rule: 'tRPC: router procedures. query for reads, mutation for writes. Zod .input() for validation. One API route for all tRPC.'
The design rule prevents: the AI creating app/api/users/route.ts in a tRPC project (tRPC has one catch-all route, not per-resource routes), writing fetch('/api/users') in a tRPC project (tRPC uses trpc.user.list.useQuery()), or defining tRPC routers in a REST project (the project uses Route Handlers, not tRPC). The API design pattern determines: every file location, every import, and every data access call.
- REST: resource URLs (GET /api/users/:id), per-resource route files in app/api/
- tRPC: router procedures (trpc.user.getById), one catch-all route in app/api/trpc/
- REST: HTTP methods for CRUD. tRPC: .query() for reads, .mutation() for writes
- REST: many route files. tRPC: one router file per domain, one API route total
- AI error: per-resource routes in tRPC project or trpc imports in REST project
REST: app/api/users/route.ts + app/api/users/[id]/route.ts + app/api/posts/route.ts = many files. tRPC: one app/api/trpc/[trpc]/route.ts catch-all. AI creating per-resource routes in a tRPC project: wrong architecture. All tRPC procedures go through one API route.
Validation and Type Safety
REST validation: Zod in each route handler. const body = createUserSchema.parse(await request.json()). The schema: validates the request. The types: inferred from the schema (z.infer<typeof createUserSchema>). The client: must define its own TypeScript types for the API response (or generate them from OpenAPI). Type drift: possible (server changes a field, client type not updated). AI rule: 'REST: Zod validation in every handler. z.infer for request types. Export response types for client use. Or: generate types from OpenAPI spec.'
tRPC validation: Zod in the .input() schema. publicProcedure.input(z.object({ name: z.string(), email: z.string().email() })).mutation(async ({ input }) => { /* input is typed */ }). The schema: validates AND types the input. The client: automatically gets the correct types (trpc.user.create.useMutation() knows the input shape). Type drift: impossible (the types are shared through the TypeScript type system). No separate client types. No OpenAPI generation. No manual type maintenance.
The type safety difference is: tRPC's primary value proposition. REST + Zod: validates at runtime, types inferred but client types are separate. tRPC + Zod: validates at runtime, types flow end-to-end automatically. For TypeScript monorepos (frontend and backend in the same repo): tRPC eliminates the type drift problem entirely. For multi-language clients (mobile app in Swift, web in TypeScript, partner API in Python): REST is necessary (tRPC only works with TypeScript clients).
- REST: Zod in handler + z.infer for types + separate client types (or OpenAPI generation)
- tRPC: Zod in .input() + types flow to client automatically — zero drift, zero maintenance
- Type drift: REST = possible (client/server types separate). tRPC = impossible (types shared)
- tRPC advantage: end-to-end type safety in TypeScript monorepos. Change server = client sees change
- REST advantage: works with any language client. tRPC: TypeScript clients only
REST: server changes a field name, client type is not updated, runtime error in production. tRPC: server changes a field name, TypeScript compile error in the client immediately. End-to-end type safety means: the type system catches API contract violations at build time, not runtime.
Client Integration: fetch vs Type-Safe Hooks
REST client integration: fetch or a typed client. const response = await fetch('/api/users/123'); const user: User = await response.json(). Manual: construct URLs, parse JSON, type the response. With TanStack Query: const { data } = useQuery({ queryKey: ['user', id], queryFn: () => fetch('/api/users/' + id).then(r => r.json()) }). Better but still: manual URL construction, manual type annotation. AI rule: 'REST client: TanStack Query with typed queryFn. Type responses explicitly. Handle errors: check response.ok before parsing.'
tRPC client integration: type-safe hooks generated from the router. const { data } = trpc.user.getById.useQuery({ id: '123' }). The data variable: automatically typed as the router procedure return type. No URL, no fetch, no JSON parsing, no type annotation. Autocomplete: shows all available procedures. Type errors: caught at compile time if the input does not match the schema. AI rule: 'tRPC client: use trpc.router.procedure.useQuery/useMutation. Types are automatic. No fetch, no manual types.'
The client rule prevents: the AI generating fetch('/api/users') in a tRPC project (use trpc.user.list.useQuery instead), manually typing API responses in a tRPC project (types are automatic), using tRPC hooks in a REST project (trpc is not configured), or constructing URLs in a tRPC project (no URLs in tRPC — procedures are function calls). The client integration pattern: differs completely between REST and tRPC. The rule aligns every data fetching call.
When to Use Each: Decision Criteria
Use REST when: your API has non-TypeScript clients (mobile apps in Swift/Kotlin, partner APIs, third-party integrations), your API is public (REST is the universal standard — any developer can call it with curl), your team prefers the HTTP convention model (resource URLs, status codes, OpenAPI documentation), or you need API gateway features (rate limiting by URL path, caching by URL, routing by path prefix — all URL-based). REST is: the universal API standard. Every tool, every language, every client supports it.
Use tRPC when: client and server are both TypeScript in the same monorepo (the type-sharing benefit only works here), the API is internal (not called by non-TypeScript clients), you want zero-drift end-to-end type safety (the primary tRPC value proposition), and you want maximum developer velocity (no API types to maintain, no OpenAPI to generate, no fetch to write). tRPC is: the TypeScript-monorepo-specific optimized path. Fastest development for the narrowest use case.
The common mistake: choosing tRPC for a public API (third-party developers cannot use tRPC — they need REST or GraphQL). Or: choosing REST for a TypeScript monorepo with only one frontend client (tRPC would be faster and type-safe with zero additional effort). Match the API paradigm to the client landscape. Multiple languages: REST. Single TypeScript monorepo: tRPC. RuleSync syncs: the API paradigm rule to every team member's AI tool.
- REST: multi-language clients, public APIs, HTTP conventions, API gateway features
- tRPC: TypeScript monorepo, internal API, end-to-end type safety, maximum velocity
- Mistake: tRPC for public API (non-TS clients cannot use it). REST for solo TS monorepo (over-engineering)
- Decision: how many client languages? 1 (TypeScript) = consider tRPC. 2+ = REST
- Both can coexist: tRPC for internal + REST endpoints for external/webhooks in the same project
1 client language (TypeScript): consider tRPC (end-to-end types, maximum velocity). 2+ client languages (TS + Swift + Python): REST is necessary (tRPC only works with TypeScript). The client landscape determines the API paradigm. Both can coexist: tRPC internal + REST for webhooks/external.
Comparison Summary
Summary of AI rules for REST vs tRPC.
- Design: REST = resource URLs + HTTP methods. tRPC = router procedures + query/mutation
- Files: REST = per-resource route files. tRPC = one router per domain + one catch-all API route
- Validation: REST = Zod in handler manually. tRPC = Zod in .input() (validation + types automatic)
- Type safety: REST = separate client types (drift possible). tRPC = end-to-end shared (zero drift)
- Client: REST = fetch + TanStack Query + manual types. tRPC = trpc.router.procedure.useQuery (automatic)
- Use REST: multi-language clients, public API. Use tRPC: TypeScript monorepo, internal API
- Coexist: tRPC for internal + REST for webhooks/external in the same project
- Rule prevents: fetch in tRPC project, trpc imports in REST project, manual types in tRPC