iosNudge

Compare REST, GraphQL and WebSockets. When would you use each?

MediumNetworkingSystem design 2 min read

TL;DR

  • REST: one URL per resource, standard HTTP methods. Simple and easy to cache. Default choice.
  • GraphQL: one endpoint. The client asks for exactly the fields it needs. Good for complex screens and many client types.
  • WebSockets: a long-lived, two-way connection. Good for chat, live prices, multiplayer.

They are not exclusive. Many apps use REST or GraphQL for data and a WebSocket for live updates.

REST

GET    /users/42          -> one user
GET    /users/42/posts    -> that user's posts
POST   /posts             -> create a post
DELETE /posts/7           -> delete a post
var request = URLRequest(url: baseURL.appending(path: "users/42"))
request.httpMethod = "GET"
let (data, _) = try await session.data(for: request)
let user = try decoder.decode(User.self, from: data)

Pros: simple, works with HTTP caching (ETag, Cache-Control), easy to debug with any tool. Cons: over-fetching (you get fields you do not need) and under-fetching (a screen needs three calls).

GraphQL

One POST /graphql endpoint. The query describes the shape of the answer.

query Profile($id: ID!) {
  user(id: $id) {
    name
    avatarURL
    posts(first: 3) { title }
  }
}
struct GraphQLRequest<V: Encodable>: Encodable {
    let query: String
    let variables: V
}

var request = URLRequest(url: baseURL.appending(path: "graphql"))
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(
    GraphQLRequest(query: profileQuery, variables: ["id": "42"])
)

In real apps, a client like Apollo iOS generates Swift types from your queries. You get type-safe models.

Pros: one request per screen, exact fields, a typed schema, easy to evolve without new versions. Cons: HTTP caching is harder (everything is a POST to one URL), errors can come with a 200 status, and the server is more complex.

WebSockets

A WebSocket starts as HTTP and upgrades to a two-way connection that stays open.

let socket = URLSession.shared.webSocketTask(with: URL(string: "wss://chat.example.com")!)
socket.resume()

try await socket.send(.string(#"{"type":"join","room":"general"}"#))

func listen() async {
    while true {
        do {
            let message = try await socket.receive()
            if case .string(let text) = message { handle(text) }
        } catch {
            reconnectWithBackoff()
            return
        }
    }
}

Things you must handle yourself:

  • Reconnection with exponential backoff.
  • Heartbeats: socket.sendPing to detect dead connections.
  • App lifecycle: iOS suspends your app in the background and the socket dies. Use push notifications for background messages.
  • Missed messages: after reconnect, fetch what you missed with a normal request.

Other real-time options

OptionDirectionNotes
PollingClient asks every N secondsSimple, wasteful
Long pollingServer holds the request until dataWorks everywhere
Server-Sent EventsServer to client onlyPlain HTTP. Read it with URLSession.bytes(for:) and lines
WebSocketBoth waysBest for chat and games
Push notificationsServer to deviceWorks when the app is not running

How to choose

NeedPick
CRUD app, public API, strong cachingREST
Many screens with nested data, several clients (iOS, Android, web)GraphQL
Live two-way updatesWebSocket
Live one-way updates (scores, logs, AI token streams)Server-Sent Events

Common follow-up questions

  • How does GraphQL cache on the client? Apollo has a normalized cache keyed by object ID.
  • What is a GraphQL subscription? Real-time updates, usually sent over a WebSocket.
  • REST versioning? Put the version in the path (/v2/) or a header.
  • How do you stream an AI response? Usually SSE: read session.bytes(for:) and handle each data: line.