API Development: REST, GraphQL, WebSocket, tRPC
A client comes with a Postman collection of 200 endpoints and says: "Everything works, but the frontend is slow". Open the Network tab — 47 sequential requests to load a single dashboard page. Each waits for the previous one. This is not a server speed problem, it's an API architecture problem.
When REST Stops Working
REST works well for simple CRUD operations. But as soon as you have a mobile app alongside a web interface, over-fetching starts: the mobile app requests /api/users/123 and gets a 4KB object, when it only needs name and avatar. Multiply by a list of 50 users — 200KB of traffic instead of 8KB.
GraphQL solves this through selection sets. The client describes exactly which fields it needs, and the server returns exactly those. On a project with React Native + Next.js we migrated from REST to Apollo Server: the payload size on the main screen of the application dropped from 340KB to 28KB without any changes in backend logic — just schema and resolvers.
Typical pain points when implementing GraphQL: N+1 query. The resolver for a post's author field calls SELECT * FROM users WHERE id = ? for each post in the list. On a page with 20 posts — 21 database queries. Solved via DataLoader — it batches queries and turns them into one SELECT * FROM users WHERE id IN (...).
tRPC: when frontend and backend are in TypeScript
If the entire stack is in TypeScript (Next.js + Node/Bun), tRPC eliminates an entire layer of problems. You define a procedure on the server — the client gets full type safety automatically, without code generation and without Swagger. Renamed a field in the Zod schema — TypeScript will highlight all places on the frontend where it's used. This is not magic, it's just types across HTTP boundaries.
tRPC is not suitable if third-party clients or mobile apps in other languages consume the API. In such cases, GraphQL or REST with OpenAPI specification.
WebSocket and Real-time
HTTP polling every 5 seconds is not real-time. This is an illusion of real-time with up to 5 seconds delay and useless load on the server. For chats, live notifications, collaborative editing — WebSocket or Server-Sent Events.
SSE vs WebSocket: SSE is a one-directional stream from server to client, works over normal HTTP, auto-reconnects. Suitable for notifications, data streaming, progress bars for long operations. WebSocket is bidirectional, needed for chats and collaborative features. In practice, 80% of "real-time" tasks are solved with SSE, not WebSocket — less infrastructure complexity.
Typical mistake: opening a WebSocket connection on each component on a page. We saw a project where 12 parallel WS connections opened on the dashboard. Correct approach — one connection manager at the application level, subscriptions through it.
Swagger / OpenAPI as a Contract
Documentation written after the fact becomes outdated the next day after release. We write the OpenAPI 3.1 specification before development starts, it becomes a contract between frontend and backend. Frontend generates types via openapi-typescript, backend validates incoming data through generated schemas. Specification mismatches with implementation are caught in CI, not in review.
For Laravel — l5-swagger or dedoc/scramble (auto-generation from PHP annotations and FormRequest). For Node.js — @fastify/swagger or Zod + zod-to-openapi.
API Authentication
JWT with long-lived access tokens without rotation — source of problems upon compromise. Correct scheme: access token for 15 minutes, refresh token for 30 days with rotation on every use. Refresh token stored in httpOnly cookie, access token — in memory (not in localStorage).
For inter-service communication — API Keys with scope limitations or mTLS. OAuth 2.0 with PKCE for public clients (SPA, mobile).
Versioning and Backward Compatibility
Breaking API changes without versioning break clients. Three approaches: URL versioning (/api/v2/), header (Accept: application/vnd.api+json;version=2), evolutionary versioning through adding fields without removing old ones. The latter works for GraphQL — deprecated directive allows gradually phasing out fields.
Work Process
Analysis starts with an audit of current integrations and composing a data schema. We design the API contract in OpenAPI or SDL (for GraphQL) — before the first line of code. Development proceeds according to the contract with auto-tests for each endpoint. Load testing via k6 before deployment: basic scenario — 500 virtual users, 10 minutes, p95 latency no higher than 200ms.
Deployment via CI/CD with mandatory backward compatibility check (oasdiff for OpenAPI). Documentation is published automatically from the specification.
Timeline
API development for a typical SaaS project with 30–50 endpoints: from 3 to 8 weeks depending on business logic complexity and number of external integrations. Migration of existing REST API to GraphQL — from 2 to 6 weeks. Adding a WebSocket layer to a ready backend — from 1 to 3 weeks.







