Every API starts with the same question: how do the client and server talk? REST is the default, GraphQL gets pitched as its replacement, and WebSockets come up whenever someone mentions "real-time." But they're not competing options for the same problem — they solve different problems. Picking the right one upfront saves significant refactoring later.
REST: The Reliable Default
REST (Representational State Transfer) isn't a protocol or a standard — it's an architectural style. In practice it means: resources are identified by URLs, you manipulate them with HTTP verbs (GET, POST, PUT, PATCH, DELETE), responses are self-contained, and the server doesn't maintain state between requests.
GET /users/42 ← fetch a user
POST /users ← create a user
PUT /users/42 ← replace a user
PATCH /users/42 ← update fields on a user
DELETE /users/42 ← delete a user
REST's biggest strength is HTTP's infrastructure: caching, CDNs, load balancers, proxies, logging — all of it understands HTTP verbs and status codes natively. A GET /articles/top can be cached at the CDN edge. A 304 Not Modified saves the full response. You get this for free.
The weakness is over-fetching and under-fetching. A GET /users/42 might return 40 fields when you need 3. A user profile page might need separate calls to /users/42, /users/42/posts, and /users/42/followers. Each round trip adds latency.
For most CRUD applications — dashboards, admin panels, e-commerce backends, content APIs — REST is the right choice. It's predictable, well-documented, and every developer on your team already knows how it works.
GraphQL: Client-Controlled Queries
GraphQL flips the model: instead of the server defining what each endpoint returns, the client specifies exactly what it needs in a query.
query {
user(id: "42") {
name
email
posts(limit: 5) {
title
publishedAt
}
}
}
This single request fetches the user's name, email, and their five most recent post titles. No more over-fetching extra fields. No more under-fetching with multiple round trips.
GraphQL delivers real benefits in specific scenarios:
- Mobile apps with constrained bandwidth — only transfer what the screen needs.
- Multiple clients with different data needs — mobile app needs 3 fields, web dashboard needs 15, both use the same API.
- Rapidly evolving frontends — add new fields without API versioning.
- Complex interconnected data — graph-like relationships between entities (users, followers, posts, comments).
The cost is complexity. You need a schema, a resolver layer, a GraphQL server library, and clients that understand it. Caching gets harder — every query hits a single endpoint (POST /graphql), so HTTP caching doesn't apply the same way. You need query-level or field-level caching logic instead. Debugging is harder too: a single 200 response can contain both data and errors simultaneously.
{
"data": { "user": { "name": "Alice" } },
"errors": [{ "message": "posts field failed", "path": ["user", "posts"] }]
}
That's a feature in GraphQL (partial responses), but it means you can't rely on HTTP status codes alone to determine success.
When to Choose GraphQL
Pick GraphQL when you have a genuinely complex data graph, multiple clients with divergent data needs, and you're prepared to invest in the tooling. Don't pick it because the docs look good or it seems modern. For simple CRUD APIs, GraphQL adds overhead with no payoff.
WebSockets: Persistent Bidirectional Connections
HTTP is fundamentally request-response. The client asks; the server answers. If you want the server to push updates — a live chat message, a stock price change, a notification — you're working against the grain of HTTP.
WebSockets solve this by upgrading an HTTP connection to a persistent, bidirectional TCP channel. After the handshake, both sides can send messages at any time, with no request-response pairing.
const ws = new WebSocket('wss://api.example.com/live');
ws.onopen = () => ws.send(JSON.stringify({ type: 'subscribe', channel: 'prices' }));
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
updatePriceDisplay(data);
};
ws.onclose = () => scheduleReconnect();
WebSockets are appropriate for:
- Live chat and messaging — messages must arrive instantly, in order.
- Multiplayer games — latency matters, state changes constantly.
- Collaborative editing — multiple users modifying the same document.
- Live dashboards — not just refreshed periodically, but pushed on change.
- Financial tickers — high-frequency updates where polling is wasteful.
The downsides are real. WebSocket connections are stateful — each connection lives on one server, which complicates horizontal scaling. You need sticky sessions or a pub/sub layer (Redis, etc.) so a message sent to server A actually reaches clients connected to server B. CDNs and HTTP caches don't apply. Connection drops require client-side reconnect logic. Debugging is harder than reading HTTP logs.
Server-Sent Events: The Middle Ground
Server-Sent Events (SSE) is often overlooked. It's a strictly one-way (server-to-client) option where the server pushes a stream of events to the client over a single HTTP connection. The client can't send data back on the same channel, but for many "push" use cases that's fine.
const source = new EventSource('/api/notifications');
source.onmessage = (event) => {
const notification = JSON.parse(event.data);
showNotification(notification);
};
SSE works over standard HTTP/2, supports automatic reconnection, and works through proxies and load balancers without special configuration. If you need the server to push updates but don't need bidirectional communication, SSE is often simpler than WebSockets. Use cases: live logs, progress bars, notification feeds, activity streams.
The Decision Framework
Start here and work down:
Use REST if:
- You're building a CRUD API
- Clients are predictable and similar
- You want caching at the HTTP layer
- Your team is small or inexperienced with GraphQL
- You're building a public API that third parties will consume
Use GraphQL if:
- You have multiple clients (mobile, web, third party) with different data needs
- Your data has complex relationships that map naturally to a graph
- You're comfortable with the tooling and schema maintenance overhead
- Over-fetching or under-fetching is causing a real performance problem
Use WebSockets if:
- Data changes faster than you'd be willing to poll (sub-second)
- You need bidirectional messaging (chat, collaborative editing, gaming)
- Latency is a hard requirement, not just a nice-to-have
Use Server-Sent Events if:
- You need server-to-client push but not client-to-server on the same channel
- You want simpler infrastructure than WebSockets
- Automatic reconnection is important
Most applications use REST for the majority of their API and add WebSockets or SSE for specific real-time features. GraphQL tends to be an all-or-nothing adoption at the API layer, though you can run it alongside a REST API during migration.
Practical Considerations
When working with any of these, data format matters. JSON is the universal lingua franca — validate and format it with the JSON Formatter when debugging. For WebSocket messages or GraphQL variables that include URL-safe encoded strings, the URL Encoder handles encoding edge cases.
For deeper reading on REST conventions, see REST API Design Best Practices. For HTTP status codes — which matter most for REST and GraphQL error handling — see HTTP Status Codes Guide.
The MDN documentation on WebSockets and Server-Sent Events covers the full browser API surface if you're implementing either from scratch.
In Practice
The most common mistake is reaching for WebSockets when polling or SSE would do. WebSockets add real operational complexity — connection management, scaling, reconnect logic — that polling sidesteps entirely. If your "real-time" feature updates every 5 seconds, polling every 5 seconds is probably fine. If it needs to update within 200ms, then you need WebSockets.
And don't adopt GraphQL just because REST feels old. A well-designed REST API with consistent naming and sensible field selection can serve you for years without the overhead of a GraphQL layer.