REST is the de facto standard for web APIs, but it is neither the only tool nor always the best one. In recent years alternatives have matured that attack very specific limitations: GraphQL was born from the problem of requesting tailored data from mobile; gRPC, from the need for high-performance internal communication; webhooks, from HTTP's inability to let the server notify the client. This lesson closes the module by drawing the complete map of integration styles, with examples applied to Aroma Store, so that you can choose with judgement and — most importantly — understand that these approaches do not compete: they coexist within the same company, each in its own place.

Contents

  1. Why a single style is not enough
  2. GraphQL: the client decides which fields it wants
  3. What problems GraphQL solves and which ones it introduces
  4. gRPC: a strong contract and high performance between services
  5. Event-driven communication: webhooks
  6. Queues, streaming and real time: SSE and WebSockets
  7. Comparison table of the four approaches
  8. Honest decision criteria
  9. Aroma Store's final architecture

  1. Why a single style is not enough

Aroma Store's communication needs are not homogeneous. Compare these four cases:

Need Characteristics Does it fit REST well?
A blog displays the catalogue Public, read-only, cacheable, unknown client Perfectly
Aroma Mobile's order screen Needs order, customer, coffee and shipping data at once So-so: several requests
The orders service queries stock 500 times a second Internal, high volume, latency-critical Mediocre: JSON and HTTP/1 overhead
Notifying SwiftShip that an order has been paid The sender is the server; the receiver is external Badly: HTTP only goes client → server

Each mismatch has a specific answer. Seeing them together gives you judgement; using them all at once for no reason gives you an ungovernable architecture.

  1. GraphQL: the client decides which fields it wants

GraphQL is a query language for APIs, created at Facebook in 2012 and published in 2015. Its three fundamental decisions:

  1. A typed schema defines all the available data and its relationships. It is the contract, and it is mandatory.
  2. A single URL (typically /graphql), which you POST to.
  3. The client writes the query: it asks for exactly the fields it needs, not one more.

The Aroma Store schema

type Coffee {
  id: ID!
  name: String!
  origin: String!
  roast: Roast!
  priceEuros: Float!
  stock: Int!
  tastingNotes: [String!]!
  reviews: [Review!]!
}

type Review {
  id: ID!
  author: String!
  rating: Int!
  comment: String
}

enum Roast { LIGHT MEDIUM DARK }

type Query {
  coffee(id: ID!): Coffee
  coffees(origin: String, roast: Roast, limit: Int): [Coffee!]!
}

The ! sign means "cannot be null". The schema is documentation, validation and contract all at once: tools read it and offer autocompletion as you write queries.

The query

For its catalogue screen, the mobile app needs only the name, the price and the average rating. It asks for it like this:

query {
  coffees(origin: "Ethiopia", limit: 10) {
    id
    name
    priceEuros
    reviews {
      rating
    }
  }
}

And it receives exactly that, with the same shape as the query:

{
  "data": {
    "coffees": [
      {
        "id": "cof_001",
        "name": "Ethiopia Yirgacheffe",
        "priceEuros": 14.50,
        "reviews": [{ "rating": 5 }, { "rating": 4 }]
      }
    ]
  }
}

Over HTTP, the actual request is a POST:

curl -X POST https://api.aromastore.example/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer TOKEN" \
  -d '{"query":"{ coffees(origin: \"Ethiopia\", limit: 10) { id name priceEuros reviews { rating } } }"}'

The REST equivalent

Achieving the same thing with the Aroma Store REST API would take several calls:

# 1. The Ethiopian coffees (returns ALL the fields of each coffee)
curl "https://api.aromastore.example/v1/coffees?origin=Ethiopia&limit=10"

# 2. Each coffee's reviews: one request per coffee
curl "https://api.aromastore.example/v1/coffees/cof_001/reviews"
curl "https://api.aromastore.example/v1/coffees/cof_007/reviews"
curl "https://api.aromastore.example/v1/coffees/cof_012/reviews"
# ... and so on for all ten

Eleven requests against one, and the first one downloads origin, roast, stock and tastingNotes that the screen does not use. That is exactly GraphQL's argument.

It is worth qualifying that, in fairness: REST has partial answers to this problem — expansion parameters (?include=reviews), field selection (?fields=name,priceEuros) and aggregate endpoints designed for one specific screen. The thing is that in REST these are ad hoc conventions that each API solves in its own way, whereas in GraphQL it is the model itself.

  1. What problems GraphQL solves and which ones it introduces

Problems it solves

Problem Explanation
Over-fetching Receiving more data than you need. The screen wants the name and the price, and receives fifteen fields.
Under-fetching (N+1 requests) One request not being enough, so N more are needed to complete the information.
Proliferation of bespoke endpoints Without GraphQL, every new screen tends to spawn its own specific endpoint.
Contract evolution Adding fields breaks nobody, because each client asks for its own; unused fields are marked as deprecated and you can measure who still uses them.
Out-of-date documentation The schema is executable and introspectable: it cannot lie.

Problems it introduces

Problem Explanation
HTTP caching Everything goes by POST to one URL. You lose browser, proxy and CDN caching, and have to replace it with client-level and field-level caching, which is far more complex.
Query complexity A client can ask for deeply nested relationships and bring the server down. You have to limit the depth, complexity and cost of each query.
N+1 in the database The flexibility is paid for in the resolver: asking for the reviews of 10 coffees can fire 11 SQL queries if batching techniques are not used.
Security and authorisation Permissions must be applied field by field, not per endpoint.
Status codes Errors arrive in an errors array with 200 OK: HTTP-based monitoring sees nothing.
Usage limits "100 requests per minute" means nothing if one query can cost a thousand times more than another.
File and binary uploads Not part of the model; they require extensions.
Learning curve Schema, resolvers, fragments, normalised caching and its own tooling.

Note something you can already recognise: in Richardson's terms, GraphQL is a level 0 — one endpoint, everything by POST, the operation inside the body. The difference from the swamp of POX is that here the choice is deliberate and comes with a typed contract, introspection and a tooling ecosystem that compensates for what is lost.

  1. gRPC: a strong contract and high performance between services

gRPC (Google, 2015) is modern RPC. Its pillars:

  • Protocol Buffers (protobuf): a compact, typed binary format for serialising messages.
  • HTTP/2 as transport, with multiplexing and compressed headers.
  • A contract in a .proto file from which client and server code is generated in more than ten languages.
  • Four communication modes, including streaming in both directions.

The contract of the Aroma Store inventory service

syntax = "proto3";

package aromastore.inventory.v1;

// Internal inventory service: consumed by the orders, catalogue
// and warehouse services. It is not reachable from the internet.
service Inventory {
  // One-off query of a coffee's stock
  rpc GetStock (GetStockRequest) returns (StockResponse);

  // Reserves units when an order is confirmed
  rpc ReserveStock (ReserveStockRequest) returns (StockResponse);

  // Continuous flow of stock changes (server streaming):
  // the internal panel subscribes and receives live updates
  rpc WatchChanges (WatchChangesRequest) returns (stream StockChange);
}

message GetStockRequest {
  string coffee_id = 1;            // the number is the position in the binary format
}

message ReserveStockRequest {
  string coffee_id = 1;
  int32  units = 2;
  string order_id = 3;
}

message StockResponse {
  string coffee_id = 1;
  int32  available = 2;
  int32  reserved = 3;
}

message StockChange {
  string coffee_id = 1;
  int32  available = 2;
  string timestamp = 3;            // ISO 8601 timestamp
}

The numbers (= 1, = 2) are not values: they are the field tags that take the place of names in the binary format. That is why protobuf is so compact — it does not send the field names — and why those numbers must never be changed once published: they are the real contract.

From Node.js, consuming it looks like a function call:

// The orders service checks the stock before confirming
const response = await inventoryClient.getStock({ coffee_id: 'cof_001' });

if (response.available < requestedUnits) {
  throw new Error('Insufficient stock for coffee ' + response.coffee_id);
}

The four gRPC modes

Mode Description Example in Aroma Store
Unary One request, one response GetStock
Server streaming One request, many responses WatchChanges: live stock changes
Client streaming Many requests, one response Bulk inventory upload after a stocktake
Bidirectional A continuous flow in both directions Real-time synchronisation with the warehouse

Strengths and limits

In favour: messages far smaller than JSON, faster serialisation, a strong contract with code generation, native streaming, excellent for high-volume internal traffic and for communication between services written in different languages.

Against: it is not directly consumable from a browser (it needs a gateway such as gRPC-Web), binary messages cannot be read with your eyes or debugged with curl, it does not make use of HTTP caching, and the tooling is poorer outside prepared environments. It is not a good candidate for a public API.

  1. Event-driven communication: webhooks

Everything we have seen so far shares one limitation: the client asks and the server answers. So how does Aroma Store notify SwiftShip that an order has been paid and is ready for collection?

The naive option is polling: SwiftShip checks every minute.

# SwiftShip asking over and over... almost always for nothing
curl "https://api.aromastore.example/v1/orders?status=paid&dateFrom=2026-08-14T09:00:00Z"

With one order every half hour and one query per minute, 98% of the requests are useless: they burn resources on both sides and the notification still arrives up to a minute late.

A webhook reverses the direction: the consumer registers a URL of its own, and the provider POSTs to it when something happens. It is, literally, "an API in reverse": now Aroma Store is the HTTP client and SwiftShip the server.

sequenceDiagram
    participant AS as Aroma Store
    participant SS as SwiftShip
    Note over SS,AS: Prior registration (once only)
    SS->>AS: POST /v1/webhooks<br/>{"url":"https://api.swiftship.example/aroma",<br/> "events":["order.paid"]}
    AS-->>SS: 201 Created
    Note over AS: The event happens
    AS->>SS: POST https://api.swiftship.example/aroma<br/>{"type":"order.paid", ...}
    SS-->>AS: 200 OK (acknowledgement)

Sending the event:

POST /aroma HTTP/1.1
Host: api.swiftship.example
Content-Type: application/json
Aroma-Event-Id: evt_9f2c
Aroma-Signature: sha256=7d38cb...

{
  "id": "evt_9f2c",
  "type": "order.paid",
  "date": "2026-08-14T09:20:11Z",
  "data": {
    "orderId": "ord_5001",
    "customerId": "cus_842",
    "totalEuros": 29.00,
    "shippingAddress": {
      "street": "Carrer de Mallorca 120",
      "city": "Barcelona",
      "postcode": "08036"
    }
  }
}

Four design decisions you will see in every serious webhook and that are worth internalising right away:

  1. Cryptographic signature (Aroma-Signature): the receiver recalculates an HMAC of the body with a shared secret and checks that it matches. Without this, anyone who knows the URL can invent events.
  2. Event identifier (Aroma-Event-Id): it lets the receiver detect duplicates. Webhooks guarantee "at least once" delivery, so the receiver must be idempotent.
  3. Retries with increasing backoff: if the receiver does not respond with a 2xx, the sender retries at ever longer intervals over hours, and raises an alert if it eventually gives up.
  4. Namespaced event type (order.paid, order.shipped, review.published): it allows selective subscription and the addition of new events without breaking anything.
Polling Webhook
Who initiates The consumer The provider
Notification latency Up to the polling interval Almost immediate
Useless requests Many None
Requirement on the consumer None Needs a publicly reachable URL
Complexity Very low Retries, signatures, duplicates
Reliability High (if it fails, it just retries) Requires careful design

Webhooks do not replace the REST API: they complement it. The usual approach is for the event to carry just enough and for the receiver to call the API afterwards to obtain the complete, up-to-date detail.

  1. Queues, streaming and real time: SSE and WebSockets

Let's complete the map with three mechanisms that will turn up in your professional life:

  • Message queues and streaming (RabbitMQ, Kafka, SQS): the internal equivalent of webhooks. The sender publishes an event to a broker and interested parties consume it at their own pace. They provide persistence, retries, ordering and total decoupling. Aroma Store would use them so that the invoicing, loyalty and analytics services can react to order.paid without the orders service even knowing they exist.
  • Server-Sent Events (SSE): a long-lived HTTP channel over which the server sends messages to the client. Unidirectional, simple, over ordinary HTTP, with automatic reconnection built into the browser. Ideal for the Aroma Store internal panel showing orders as they come in.
  • WebSockets: a persistent bidirectional channel over a connection upgraded from HTTP. Necessary when both ends talk continuously: a customer service chat, for example.
Mechanism Direction Over HTTP Typical case
Webhook Server → another server Yes (POST) Integration between companies
Queue / streaming Producer → consumers No Events between internal services
SSE Server → browser Yes Live panel, notifications
WebSocket Bidirectional Only the handshake Chat, real-time collaboration

A useful rule: if the user has to find out about something without asking for it, you need one of these four; no REST, GraphQL or unary gRPC API solves it on its own.

  1. Comparison table of the four approaches

Criterion REST GraphQL gRPC Webhooks / events
Model Resources and HTTP verbs Queries over a schema Procedure calls Notification of occurrences
Transport HTTP HTTP (POST) HTTP/2 HTTP (POST) or broker
Format JSON (or others) JSON Binary (protobuf) JSON
Contract Optional (OpenAPI) Mandatory (schema) Mandatory (.proto) Documented per event
Typing Weak unless there is a schema Strong Strong Weak
Coupling Low Medium High (shared contract) Very low
HTTP caching Native and free Difficult Not applicable Not applicable
Streaming No (use SSE/WS) With subscriptions Native, in 4 modes Asynchronous by nature
From a browser Direct Direct Needs a gateway Not applicable
Debugging curl, browser Its own tooling Needs tooling Delivery log
Target audience Anyone, third parties included Your own clients with rich screens Internal services Partners and integrations
Learning curve Gentle Medium-high Medium-high Medium (reliability)
Strong point Simplicity, caching, universality Tailored data in one call Performance and a strong contract Notifications without polling
Weak point Over/under-fetching Caching and cost control Not suitable for the public Delivery and duplicates

  1. Honest decision criteria

Concrete questions, in order of importance:

  1. Who consumes the API? If it is third parties you do not control, REST. The lowest barrier to entry wins almost every time; a public API in gRPC would be a mistake.
  2. Is the data cacheable? If the catalogue is queried thousands of times and changes little, REST's free HTTP caching is a weighty argument that is hard to match.
  3. Do your clients need highly variable combinations of data? If you have many different screens over the same model and genuinely suffer from over/under-fetching, GraphQL delivers real value.
  4. Is it internal communication with high volume and critical latency? gRPC. You control both ends, so the contract coupling does not hurt and the performance is noticeable.
  5. Is the server the sender? Webhooks outwards, queues inwards. There is no debate: client-server HTTP does not cover this case.
  6. What is the size and experience of your team? An excellent architecture nobody knows how to operate is worse than a good one everybody understands. Badly operated GraphQL is an inexhaustible source of performance incidents.

Three warnings, from the industry's accumulated experience:

  • Do not adopt GraphQL just to avoid two requests. With HTTP/2 several small requests are cheap, and REST supports expansion parameters and field selection.
  • Do not use gRPC facing outwards unless your consumers are technical teams capable of integrating it.
  • Do not implement webhooks without signatures, retries and idempotency. A badly built webhook produces duplicated orders or lost events, and both show up in the accounts.

  1. Aroma Store's final architecture

With the whole map on the table, here is the team's decision, and it is the one we will follow for the rest of the course:

graph TD
    subgraph Outside
        W["Web shop (SPA)"]
        M["Aroma Mobile"]
        B["Blogs and comparison sites"]
        SS["SwiftShip"]
    end

    subgraph "Public and internal API"
        API["REST API v1<br/>Node.js 20 + Express<br/><i>api.aromastore.example/v1</i>"]
    end

    subgraph "Internal services"
        INV["Inventory service"]
        BIL["Invoicing service"]
    end

    W -->|REST/JSON| API
    M -->|REST/JSON| API
    B -->|public REST/JSON| API
    API -->|gRPC| INV
    API -->|gRPC| BIL
    API -->|"webhook: order.paid"| SS
Need Technology chosen Reason
Public catalogue and reviews API REST + JSON Frictionless adoption, cacheable, testable with curl
Web, mobile app and internal panel REST + JSON A single stable contract for three in-house clients
Stock queries between services gRPC High volume, low latency, strong contract, both ends in-house
Notifications to SwiftShip Signed webhooks The sender is the server; it avoids constant polling
Live orders in the internal panel SSE Unidirectional and simple, over standard HTTP
Events between internal services Message queue Decouples invoicing, loyalty and analytics

And an equally important decision: GraphQL, for now, no. The team has assessed it and concluded that its screens are few and stable, that the catalogue cache is an asset it does not want to lose and that the team is small. It is a revisable decision, taken with arguments and written down. That is what designing looks like; the opposite is following fashion.

Common Mistakes and Tips

  • Choosing by fashion rather than by problem. Always ask what specific limitation you are suffering from today. If you cannot name it, do not change technology.
  • Believing that GraphQL replaces REST. They are complementary. Many companies expose GraphQL for their own clients and REST for third parties, over the same backend.
  • Forgetting that GraphQL loses HTTP caching. If your traffic is mostly reads of slowly changing data, that loss can cost more than you save in requests.
  • Using gRPC in the browser without a gateway. It does not work directly: you need gRPC-Web and a proxy that translates.
  • Changing the field numbers in a .proto. It breaks binary compatibility silently and in a way that is hard to diagnose. The numbers are the contract.
  • Treating a webhook as a guaranteed, single delivery. It arrives at least once, and sometimes more. Without idempotency in the receiver, you will get duplicates.
  • Putting complete, sensitive data in the webhook body. Send the minimum and let the receiver query the API if it needs more: the event may arrive late and with data that is already out of date.
  • Tip: keep a simple rule — REST outwards, gRPC inwards, events for anything asynchronous — and deviate from it only with a reason you can write down in two lines.

Exercises

Exercise 1: choose the right technology

For each Aroma Store need, choose between REST, GraphQL, gRPC, a webhook or SSE, and justify it with two arguments:

  1. An external price comparison site wants to query the catalogue every hour.
  2. The orders service checks stock 800 times a second before confirming purchases.
  3. Aroma Mobile's "my account" screen shows the customer's details, their last three orders, the shipping status of each one and their reviews.
  4. The roasting supplier must find out as soon as a coffee's stock drops below 20 units.
  5. The warehouse panel shows orders as they come in, without reloading.

Exercise 2: design a complete webhook

Design the order.shipped webhook that Aroma Store will send to any customer who requests it. Specify: the event's JSON body, the necessary headers, what the receiver must respond, what the sender will do if it does not respond and which security measures you include.

Exercise 3: compare request cost

The coffee detail screen in Aroma Mobile needs: the name, price, stock, the last five reviews (author and rating) and the roaster's name.

  1. How many REST requests would a naive design take?
  2. Write the equivalent GraphQL query.
  3. Propose two solutions within REST that reduce the number of requests without adopting GraphQL.
  4. What is lost in each case?

Solutions

Solution 1

  1. REST. It is an unknown third party that must be able to integrate without friction, and the catalogue is public, cacheable content: Cache-Control means many of those queries never even reach the server.
  2. gRPC. It is internal traffic with both ends under your control, where the binary format and HTTP/2 reduce latency and CPU compared with JSON; in addition the .proto contract prevents type errors on a critical path.
  3. GraphQL would be the ideal candidate, as it combines four data sources into a single query, avoiding the N+1 request problem. That said, if it is the only screen with that problem, the pragmatic answer is an aggregate REST endpoint (GET /v1/customers/cus_842/summary): it solves the case without introducing a whole technology.
  4. Webhook. The sender is Aroma Store and the receiver is an external company; constant polling would be wasteful and would add delay to the notification.
  5. SSE. It is a unidirectional flow from the server to the browser, it works over ordinary HTTP and it reconnects on its own. A WebSocket would be needlessly complex because the panel sends nothing back.

Solution 2

Event body:

{
  "id": "evt_a41d",
  "type": "order.shipped",
  "date": "2026-08-15T11:04:00Z",
  "version": "1",
  "data": {
    "orderId": "ord_5001",
    "customerId": "cus_842",
    "carrier": "SwiftShip",
    "trackingNumber": "SS9928374ES",
    "estimatedDelivery": "2026-08-17"
  }
}

Headers:

POST /webhooks/aroma HTTP/1.1
Content-Type: application/json
Aroma-Event-Id: evt_a41d
Aroma-Event-Type: order.shipped
Aroma-Signature: sha256=7d38cb...
Aroma-Date: 2026-08-15T11:04:00Z
User-Agent: AromaStore-Webhooks/1.0

What the receiver must respond: a 2xx (ideally 200 OK or 204 No Content) as soon as possible, before processing the event. The rule is accept, enqueue and process asynchronously: if you take a long time to respond because you are doing heavy work, the sender may treat it as a failure and retry, generating duplicates.

If it does not respond: retries with increasing backoff (for example, after 30 s, 2 min, 10 min, 1 h, 6 h and 24 h), a log of every attempt available to the consumer, an email alert after several failures and deactivation of the endpoint after a number of consecutive failures.

Security:

  • HMAC-SHA256 signature of the body with a shared secret, which the receiver verifies before processing anything.
  • A timestamp in the signature to reject old resends (replay attacks).
  • HTTPS mandatory on the destination URL.
  • An event identifier so the receiver can discard duplicates.
  • Minimal data: no payment data and no unnecessary personal data; if more is needed, let the receiver query the API with authentication.

Solution 3

1. REST requests with a naive design: three.

curl https://api.aromastore.example/v1/coffees/cof_001
curl "https://api.aromastore.example/v1/coffees/cof_001/reviews?limit=5"
curl https://api.aromastore.example/v1/roasters/ros_03

2. GraphQL query: one.

query {
  coffee(id: "cof_001") {
    name
    priceEuros
    stock
    roaster { name }
    reviews(limit: 5) { author rating }
  }
}

3. Two solutions within REST:

  • An expansion parameter: GET /v1/coffees/cof_001?include=reviews,roaster, which returns the related resources embedded in the same response. A single request, and it is still a cacheable GET.
  • A screen-oriented aggregate resource: GET /v1/coffees/cof_001/detail, designed specifically for that view. Simple and very efficient.

4. What is lost in each case:

  • With GraphQL: HTTP caching (everything is a POST to /graphql), meaningful status codes and the ability to try the call from the browser; on top of that, query cost has to be controlled.
  • With the expansion parameter: the cache fragments (each combination of include is a separate entry) and the server gets more complicated; if it is overused, you end up reimplementing GraphQL by hand, and worse.
  • With the aggregate resource: the API becomes coupled to one specific screen. If every new view adds its own endpoint, the API fills up with bespoke resources that are hard to maintain and to document.

There is no free option: all three are conscious trade-offs, and choosing well means knowing which one hurts least in your context.

Conclusion

You now have the complete map of integration styles. REST stands out for its simplicity, its universality and the caching it inherits from HTTP, and it is the natural choice when the consumer could be anyone. GraphQL solves over-fetching and under-fetching by giving the client control over the fields, in exchange for losing HTTP caching and having to govern query cost. gRPC brings a strong contract, a binary format and streaming, and shines between internal services where you control both ends. And webhooks, together with queues, SSE and WebSockets, cover the gap that no request-response model can cover: letting the server take the initiative. The practical conclusion is that they do not compete: Aroma Store will use REST outwards, gRPC inwards and webhooks for its integrations, and it has left GraphQL out for reasons that are written down and revisable.

With this we close module 1. You know what an API is and who it is designed for, where today's ecosystem comes from, how HTTP works underneath, what the six REST constraints consist of, how to measure an API's maturity and what alternatives exist. It is time to move from understanding to building. In module 2, RESTful API Design, we will start designing the Aroma Store API piece by piece: its design principles, its resources and URIs, the precise use of each HTTP method and each status code, content negotiation, filtering and pagination, versioning and documentation. That is the moment when the ideas in this module turn into concrete decisions about a real contract.

REST API Course: Principles of Designing and Developing RESTful APIs

Module 1: Introduction to RESTful APIs

Module 2: Designing RESTful APIs

Module 3: Building RESTful APIs

Module 4: Best Practices and Security

Module 5: Tools and Frameworks

Module 6: Case Studies and Projects

© Copyright 2026. All rights reserved