# REST vs GraphQL vs gRPC

Course: [API](https://www.devtutorials.ai/learn/building-apis) · Section: Beyond REST
Source: https://www.devtutorials.ai/learn/building-apis/beyond-rest/choosing-a-style

> Start with REST, and pick GraphQL, gRPC, webhooks or Server-Sent Events only when you have a clear reason.

**Choosing an API style** means selecting the best way for programs to communicate.

Different styles solve different problems:

- **REST** is simple, widely supported, and great for CRUD operations.
- **GraphQL** lets clients request exactly the data they need.
- **gRPC** provides fast communication between services.
- **Webhooks** let servers notify other servers when something happens.
- **Server-Sent Events (SSE)** let servers send live updates to web pages.

There is no single best choice for every situation. The best API style depends on the problem you are trying to solve.

## Watch a Partner Ask Ten Times

A delivery partner wants to know when order `101` is ready. The only available API is:

```http
GET /orders/101
```

The partner repeatedly asks: Is it ready yet?

This technique is called **polling**. Press **Run**.

Results:

- Ten requests are sent.
- The first nine responses say:

```json
{
  "status": "preparing",
  "changed": false
}
```

- The tenth response finally says:

```json
{
  "status": "ready",
  "changed": true
}
```

Statistics:

```json
{
  "requests": 10,
  "useful": 1,
  "wasted": 9
}
```

Nine requests provided no new information. As the number of partners grows, thousands of unnecessary requests are generated.

**In simple words:** polling is useful when the client needs information right now, but it can waste resources when the client is only waiting for future changes.

## TL;DR

- **REST** is the default choice for most APIs.
- **GraphQL** works well when screens need different combinations of data.
- **gRPC** is ideal for high-performance communication between services.
- **Webhooks** push events to other servers.
- **SSE** pushes updates to web pages over HTTP.
- Choose a specialized style only when it clearly solves a problem that REST does not.

## A Real-World Example

Think about different ways of transporting something.

### REST = Car

A car:

- Works almost everywhere.
- Uses existing roads.
- Is easy for most people.

REST has similar strengths:

- Simple
- Flexible
- Universally supported

### gRPC = Train

A train:

- Travels very quickly.
- Carries lots of passengers.
- Requires dedicated infrastructure.

gRPC is similar:

- Very efficient
- Extremely fast
- Requires a shared contract and supporting tools

### Webhook or SSE = Courier

A courier delivers information when it is ready. You do not repeatedly visit a shop asking: Is my package ready now?

Instead, the courier contacts you. This is how webhooks and SSE work.

### GraphQL = Smart Shopping Trip

GraphQL lets you request exactly what you need in a single trip. No extra items. No unnecessary stops.

## What You Will Learn

In this lesson, you will learn:

1. Why polling can waste resources.
2. When REST is the best choice.
3. When GraphQL reduces over-fetching.
4. When gRPC improves performance.
5. How webhooks push events securely.
6. How SSE delivers live updates.
7. How to choose the right style for a problem.

## Same Data, Three Ways

Suppose the Brew & Bytes home screen needs:

- Customer name
- Loyalty points
- Two orders
- Featured drink

Let's retrieve that information using three different API styles.

### REST

Results:

- 4 calls
- 419 bytes

The screen requests multiple endpoints.

Examples:

```http
GET /customers/me
GET /loyalty/me
GET /orders
GET /featured-drink
```

The API also sends fields that the screen does not need.

Examples:

```json
{
  "email": "...",
  "calories": 220
}
```

REST is simple but may return extra data.

### GraphQL

Results:

- 1 call
- 157 bytes

GraphQL lets the client specify the exact fields it wants.

Example:

```graphql
query {
  customer {
    name
    points
  }
}
```

Only requested fields are returned.

Benefits:

- Fewer requests
- Smaller responses
- Flexible for UI development

### gRPC

Results:

- 1 call
- 44 bytes

Instead of JSON, gRPC uses:

```text
Protocol Buffers (protobuf)
```

Data is encoded in binary format. Field names are not transmitted. Both sides already know the structure from a shared `.proto` file.

Benefits:

- Small payloads
- Fast serialization
- Efficient network usage

Smaller responses are not automatically better. REST responses are easy to inspect and debug. gRPC is harder for humans to read and requires specialized tooling. Because browsers cannot directly use standard gRPC, it is commonly used between backend services rather than public APIs.

## Push Updates to Another Server with Webhooks

Polling repeatedly asks for updates. A webhook reverses the flow. Instead of: Partner → API → Partner → API → Partner → API

the partner supplies a callback URL once. Later, when something happens, the API sends a request to that URL.

Example:

```http
POST https://partner.example/webhook
```

when an order ships.

### Webhook Security

Anyone could try to send fake events.

To prevent this, webhook payloads are signed. Both parties share a secret.  The sender computes:

```text
HMAC-SHA256(secret, body)
```

and includes the result in:

```http
X-Signature
```

Process:

1. Sender signs the body.
2. Receiver computes its own signature.
3. Signatures are compared.
4. Matching signatures mean the payload is authentic.

If the body changes:

```json
{
  "orderId": 101
}
```

to:

```json
{
  "orderId": 102
}
```

the signature no longer matches. The webhook is rejected.

Never compare signatures using normal string comparison. Use fixed-time comparison functions to prevent timing attacks. Also sign and verify the exact raw request body bytes. Even small formatting changes can produce different signatures.

### Production Webhook Features

Most webhook systems also include:

- Event IDs
- Retries
- Delivery tracking
- Timestamps
- Replay protection

Examples include GitHub, Stripe, Shopify, and many SaaS platforms.

## Push Updates to a Web Page with SSE

Servers can push updates directly to browser pages using: Server-Sent Events (SSE)

Unlike webhooks, browsers do not have public URLs. Instead, the browser keeps one HTTP connection open.

Response:

```http
Content-Type: text/event-stream
```

Event format:

```text
event: status
id: 1
data: {"status":"preparing"}

```

The blank line ends the event. Browser code:

```js
const events = new EventSource("/orders/101/events");

events.addEventListener("status", e => {
  console.log(JSON.parse(e.data));
});
```

Benefits:

- Simple
- Uses normal HTTP
- Automatically reconnects

Good use cases:

- Order tracking
- Notifications
- Monitoring dashboards
- AI response streaming

SSE is one-way communication. If both client and server must send messages continuously, use WebSockets instead. In .NET applications this is commonly done with SignalR.

## A Simple Way to Choose

Choose the simplest option that solves the problem.

### Public API

Use: REST

Reason:

- Familiar
- Easy to document
- Works everywhere

### Admin CRUD Application

Use: REST

Reason:

- Standard resource operations

### Mobile Home Screen

Use: GraphQL

Reason:

- Different screens need different data shapes

### Internal Payment Service

Use: gRPC

Reason:

- Low latency
- High performance

### Hardware Sensor Stream

Use: gRPC

Reason:

- Efficient streaming

### Live Order Tracking

Use: SSE

Reason:

- Server pushes updates to the browser

### Partner Notifications

Use: Webhook

Reason:

- Server notifies another server

### Browser Chat

Use: WebSocket

Reason:

- Two-way communication

Most modern systems combine multiple styles.

Example:

```text
Public API      → REST
Internal APIs   → gRPC
Partner Events  → Webhooks
Live UI Updates → SSE
```

## Long-Running Jobs with 202 Accepted

Some work takes too long to finish during a single request.

Examples:

- Large reports
- Video processing
- Data imports

Do not keep the request open for minutes.

Instead return:

```http
202 Accepted
```

### Start the Job

Request:

```http
POST /reports
```

Response:

```http
202 Accepted
Location: /jobs/1
```

Body:

```json
{
  "status": "queued"
}
```

### Check Progress

Request:

```http
GET /jobs/1
```

Response:

```json
{
  "status": "running"
}
```

and:

```http
Retry-After: 5
```

### Job Finished

Response:

```json
{
  "status": "done",
  "result": "/reports/1"
}
```

The report can then be downloaded. For large partner integrations, combine jobs with webhooks so clients do not need to poll repeatedly.

## REST vs GraphQL vs gRPC

| Style | Best For | Strengths | Trade-offs |
|---|---|---|---|
| REST | Public APIs, CRUD | Simple, well-known, cache-friendly | May require multiple requests |
| GraphQL | Flexible UI screens | Exact data needed, fewer requests | More complex server implementation |
| gRPC | Service-to-service communication | Fast, compact, strongly typed | Less browser-friendly |
| Webhook | Server notifications | No polling required | Requires public callback URLs |
| SSE | Live browser updates | Simple streaming over HTTP | One-way communication |
| WebSocket | Real-time two-way communication | Interactive communication | More complex connection management |

## Where You See This

- Stripe and GitHub provide REST APIs and signed webhooks.
- Shopify and GitHub expose GraphQL APIs.
- Many large companies use gRPC internally between services.
- AI chat applications stream responses using SSE.
- Real-time messaging platforms rely on WebSockets.

## Common mistakes
| Symptom | Cause | Fix |
|---|---|---|
| Large numbers of requests with little useful data | Polling for updates | Use webhooks or SSE |
| Fake events are processed | Webhook signatures are not verified | Validate HMAC signatures |
| Valid webhook signatures fail | The body changes before verification | Verify the exact raw body |
| SSE events arrive only when the connection closes | Response buffering is enabled | Flush each event and disable buffering |
| Browsers cannot call the service | Plain gRPC is exposed publicly | Use REST, GraphQL, or gRPC-Web |
| Long-running requests time out | Expensive work runs inside the request | Return `202 Accepted` and track job status |

**Key takeaway:** Start with **REST**. Move to **GraphQL**, **gRPC**, **Webhooks**, **SSE**, or **WebSockets** only when a specific requirement makes them the better choice. REST is the default; the others are specialized tools for specialized problems.

## Exercises

## What to Remember

- **REST is the default.** Choose another style only for a reason that you can say in one sentence.
- Use **GraphQL** for many screen shapes, and **gRPC** between your own services.
- When clients wait for news, **push**: **webhooks** to servers, **SSE** to pages, and **WebSockets** for two-way talk.
- **Sign webhooks** with HMAC-SHA256, and verify them in fixed time.
- Real systems often **mix** styles, each one where it fits best.

**Next:** Capstone Project. You will put everything together, and you will build a complete Brew &amp; Bytes API from start to finish.

var app = builder.Build();

// The kitchen moves order 101 to "ready" only on the 10th check.
// A partner app does not know when, so it asks again and again: this is polling.
int polls = 0, useful = 0;
string lastSeen = "preparing"; // what the partner already knows

app.MapGet("/orders/{id}/status", (int id) =>
{
    polls++;
    string status = polls < 10 ? "preparing" : "ready";
    bool changed = status != lastSeen;
    if (changed) { useful++; lastSeen = status; }
    return Results.Ok(new { orderId = id, status, changed, poll = polls });
});

// How many checks were worth making?
app.MapGet("/stats", () => Results.Ok(new { requests = polls, useful, wasted = polls - useful }));

app.Run();`;

const app = new Hono();

// The kitchen moves order 101 to "ready" only on the 10th check.
// A partner app does not know when, so it asks again and again: this is polling.
let polls = 0, useful = 0;
let lastSeen = "preparing"; // what the partner already knows

app.get("/orders/:id/status", (c) => {
  polls++;
  const status = polls < 10 ? "preparing" : "ready";
  const changed = status !== lastSeen;
  if (changed) { useful++; lastSeen = status; }
  return c.json({ orderId: Number(c.req.param("id")), status, changed, poll: polls }, 200);
});

// How many checks were worth making?
app.get("/stats", (c) => c.json({ requests: polls, useful, wasted: polls - useful }, 200));

###
GET /orders/101/status

###
GET /orders/101/status

###
GET /orders/101/status

###
GET /orders/101/status

###
GET /orders/101/status

###
GET /orders/101/status

###
GET /orders/101/status

###
GET /orders/101/status

###
GET /orders/101/status

###
GET /stats`;

// The same home screen data, fetched three ways. We count calls and bytes.
string[] rest =
{
    """{"id":7,"name":"Asha Rao","email":"asha@example.com","phone":"+91 98450 12345","address":"12 MG Road, Bengaluru"}""",
    """[{"id":101,"item":"Latte","quantity":2,"total":440,"status":"delivered"},{"id":103,"item":"Blueberry Muffin","quantity":1,"total":150,"status":"delivered"}]""",
    """{"id":4,"name":"Cold Coffee","price":180,"description":"Chilled coffee with milk and ice","calories":210}""",
    """{"customerId":7,"points":120,"tier":"Silver"}"""
};
string graphql = """{"data":{"customer":{"name":"Asha Rao","points":120,"orders":[{"id":101,"total":440},{"id":103,"total":150}]},"featured":{"name":"Cold Coffee","price":180}}}""";

// gRPC sends protobuf: each field is a small tag number plus the value, no field names.
var order1 = Msg(Int(1, 101), Int(2, 440));
var order2 = Msg(Int(1, 103), Int(2, 150));
var featured = Msg(Str(1, "Cold Coffee"), Int(2, 180));
var proto = Msg(Str(1, "Asha Rao"), Int(2, 120), Sub(3, order1), Sub(3, order2), Sub(4, featured));

int restBytes = rest.Sum(r => Encoding.UTF8.GetByteCount(r));
Console.WriteLine($"REST     calls: {rest.Length}  bytes: {restBytes}  (JSON, extra fields)");
Console.WriteLine($"GraphQL  calls: 1  bytes: {Encoding.UTF8.GetByteCount(graphql)}  (JSON, only asked fields)");
Console.WriteLine($"gRPC     calls: 1  bytes: {proto.Length}  (protobuf, binary)");
Console.WriteLine("gRPC bytes: " + Convert.ToHexString(proto).ToLower());

// ---- A tiny protobuf encoder: varints, strings and nested messages ----
static byte[] Varint(int v) { var b = new List<byte>(); while (v > 127) { b.Add((byte)(v & 127 | 128)); v >>= 7; } b.Add((byte)v); return b.ToArray(); }
static byte[] Int(int tag, int v) => [.. Varint(tag << 3), .. Varint(v)];
static byte[] Bytes(int tag, byte[] data) => [.. Varint(tag << 3 | 2), .. Varint(data.Length), .. data];
static byte[] Str(int tag, string s) => Bytes(tag, Encoding.UTF8.GetBytes(s));
static byte[] Sub(int tag, byte[] msg) => Bytes(tag, msg);
static byte[] Msg(params byte[][] fields) => fields.SelectMany(f => f).ToArray();`;

const rest = [
  \`{"id":7,"name":"Asha Rao","email":"asha@example.com","phone":"+91 98450 12345","address":"12 MG Road, Bengaluru"}\`,
  \`[{"id":101,"item":"Latte","quantity":2,"total":440,"status":"delivered"},{"id":103,"item":"Blueberry Muffin","quantity":1,"total":150,"status":"delivered"}]\`,
  \`{"id":4,"name":"Cold Coffee","price":180,"description":"Chilled coffee with milk and ice","calories":210}\`,
  \`{"customerId":7,"points":120,"tier":"Silver"}\`,
];
const graphql = \`{"data":{"customer":{"name":"Asha Rao","points":120,"orders":[{"id":101,"total":440},{"id":103,"total":150}]},"featured":{"name":"Cold Coffee","price":180}}}\`;

// ---- A tiny protobuf encoder: varints, strings and nested messages ----
const utf8 = (s: string) => [...new TextEncoder().encode(s)];
function varint(v: number): number[] { const b: number[] = []; while (v > 127) { b.push((v & 127) | 128); v >>= 7; } b.push(v); return b; }
const int = (tag: number, v: number) => [...varint(tag << 3), ...varint(v)];
const bytes = (tag: number, data: number[]) => [...varint((tag << 3) | 2), ...varint(data.length), ...data];
const str = (tag: number, s: string) => bytes(tag, utf8(s));
const sub = (tag: number, msg: number[]) => bytes(tag, msg);
const msg = (...fields: number[][]) => fields.flat();

// gRPC sends protobuf: each field is a small tag number plus the value, no field names.
const order1 = msg(int(1, 101), int(2, 440));
const order2 = msg(int(1, 103), int(2, 150));
const featured = msg(str(1, "Cold Coffee"), int(2, 180));
const proto = msg(str(1, "Asha Rao"), int(2, 120), sub(3, order1), sub(3, order2), sub(4, featured));

const restBytes = rest.reduce((n, r) => n + utf8(r).length, 0);
console.log(\`REST     calls: \${rest.length}  bytes: \${restBytes}  (JSON, extra fields)\`);
console.log(\`GraphQL  calls: 1  bytes: \${utf8(graphql).length}  (JSON, only asked fields)\`);
console.log(\`gRPC     calls: 1  bytes: \${proto.length}  (protobuf, binary)\`);
console.log("gRPC bytes: " + proto.map((b) => b.toString(16).padStart(2, "0")).join(""));`;

using System.Text;

// Brew & Bytes and the partner share one secret. Only they know it.
byte[] secret = Encoding.UTF8.GetBytes("whsec_demo_brew_and_bytes");

// SENDER: sign the exact bytes of the body and put the signature in a header.
string Sign(string body) =>
    "sha256=" + Convert.ToHexString(HMACSHA256.HashData(secret, Encoding.UTF8.GetBytes(body))).ToLower();

// RECEIVER: sign the body again and compare. Use a fixed-time compare, not ==.
bool Verify(string body, string header) =>
    CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(Sign(body)), Encoding.UTF8.GetBytes(header));

string payload = """{"event":"order.shipped","orderId":101,"status":"shipped"}""";
string header = Sign(payload);
Console.WriteLine("POST https://partner.example.com/hooks");
Console.WriteLine("X-Signature: " + header);
Console.WriteLine(payload);
Console.WriteLine();
Console.WriteLine("Receiver checks the real body:     " + (Verify(payload, header) ? "valid" : "REJECTED"));

// Someone in the middle changes the order id. The old signature no longer fits.
string tampered = payload.Replace("101", "102");
Console.WriteLine("Receiver checks a changed body:    " + (Verify(tampered, header) ? "valid" : "REJECTED"));`;

const enc = new TextEncoder();
const key = await crypto.subtle.importKey(
  "raw", enc.encode("whsec_demo_brew_and_bytes"), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
const hex = (b: ArrayBuffer) => [...new Uint8Array(b)].map((x) => x.toString(16).padStart(2, "0")).join("");

// SENDER: sign the exact bytes of the body and put the signature in a header.
async function sign(body: string) {
  return "sha256=" + hex(await crypto.subtle.sign("HMAC", key, enc.encode(body)));
}

// RECEIVER: sign the body again and compare. crypto.subtle.verify compares in fixed time.
async function verify(body: string, header: string) {
  if (!header.startsWith("sha256=")) return false;
  const sig = new Uint8Array(header.slice(7).match(/../g)!.map((h) => parseInt(h, 16)));
  return crypto.subtle.verify("HMAC", key, sig, enc.encode(body));
}

const payload = \`{"event":"order.shipped","orderId":101,"status":"shipped"}\`;
const header = await sign(payload);
console.log("POST https://partner.example.com/hooks");
console.log("X-Signature: " + header);
console.log(payload);
console.log("");
console.log("Receiver checks the real body:     " + ((await verify(payload, header)) ? "valid" : "REJECTED"));

// Someone in the middle changes the order id. The old signature no longer fits.
const tampered = payload.replace("101", "102");
console.log("Receiver checks a changed body:    " + ((await verify(tampered, header)) ? "valid" : "REJECTED"));`;

var app = builder.Build();

string[] steps = { "received", "preparing", "ready" };

// Server-Sent Events: one long response, and the server writes small events into it.
// Each event is "event:", "id:" and "data:" lines, then an empty line.
app.MapGet("/orders/{id}/events", (int id) =>
{
    var body = "";
    for (int i = 0; i < steps.Length; i++)
    {
        body += "event: status\\n";
        body += $"id: {i + 1}\\n";
        body += "data: {\\"orderId\\":" + id + ",\\"status\\":\\"" + steps[i] + "\\"}\\n\\n";
    }
    // A real stream sends each event when it happens. Here all three arrive at once.
    return Results.Text(body, "text/event-stream");
});

app.Run();`;

const app = new Hono();

const steps = ["received", "preparing", "ready"];

// Server-Sent Events: one long response, and the server writes small events into it.
// Each event is "event:", "id:" and "data:" lines, then an empty line.
app.get("/orders/:id/events", (c) => {
  const id = Number(c.req.param("id"));
  let body = "";
  for (let i = 0; i < steps.length; i++) {
    body += "event: status\\n";
    body += \`id: \${i + 1}\\n\`;
    body += \`data: {"orderId":\${id},"status":"\${steps[i]}"}\\n\\n\`;
  }
  // A real stream sends each event when it happens. Here all three arrive at once.
  return c.body(body, 200, { "Content-Type": "text/event-stream" });
});

Accept: text/event-stream`;

var jobs = new List<Job>
{
    new("Public partner API"),
    new("Mobile home screen", ManyShapes: true),
    new("Internal payment service", Internal: true),
    new("Live order tracking", LiveToBrowser: true),
    new("Tell partner: order shipped", NotifyServer: true),
    new("Admin menu CRUD"),
    new("Browser chat with barista", TwoWay: true),
    new("IoT fridge sensor stream", Internal: true, Stream: true),
};

foreach (var job in jobs)
{
    var (style, why) = Choose(job);
    Console.WriteLine($"{job.Name,-30} {style,-10} {why}");
}

// The order of the rules matters: the most specific need wins, REST is the default.
static (string, string) Choose(Job j)
{
    if (j.TwoWay) return ("WebSocket", "both sides talk at any time");
    if (j.NotifyServer) return ("Webhook", "we call them when it happens, no polling");
    if (j.LiveToBrowser) return ("SSE", "server pushes updates over plain HTTP");
    if (j.Internal && j.Stream) return ("gRPC", "fast binary stream between services");
    if (j.Internal) return ("gRPC", "strict contract, fast, both sides are ours");
    if (j.ManyShapes) return ("GraphQL", "each screen asks for its own fields");
    return ("REST", "simple, cacheable, every tool knows it");
}

record Job(string Name, bool Internal = false, bool ManyShapes = false, bool LiveToBrowser = false,
           bool NotifyServer = false, bool TwoWay = false, bool Stream = false);`;

type Job = { name: string; internal?: boolean; manyShapes?: boolean; liveToBrowser?: boolean;
             notifyServer?: boolean; twoWay?: boolean; stream?: boolean };

const jobs: Job[] = [
  { name: "Public partner API" },
  { name: "Mobile home screen", manyShapes: true },
  { name: "Internal payment service", internal: true },
  { name: "Live order tracking", liveToBrowser: true },
  { name: "Tell partner: order shipped", notifyServer: true },
  { name: "Admin menu CRUD" },
  { name: "Browser chat with barista", twoWay: true },
  { name: "IoT fridge sensor stream", internal: true, stream: true },
];

// The order of the rules matters: the most specific need wins, REST is the default.
function choose(j: Job): [string, string] {
  if (j.twoWay) return ["WebSocket", "both sides talk at any time"];
  if (j.notifyServer) return ["Webhook", "we call them when it happens, no polling"];
  if (j.liveToBrowser) return ["SSE", "server pushes updates over plain HTTP"];
  if (j.internal && j.stream) return ["gRPC", "fast binary stream between services"];
  if (j.internal) return ["gRPC", "strict contract, fast, both sides are ours"];
  if (j.manyShapes) return ["GraphQL", "each screen asks for its own fields"];
  return ["REST", "simple, cacheable, every tool knows it"];
}

for (const job of jobs) {
  const [style, why] = choose(job);
  console.log(\`\${job.name.padEnd(30)} \${style.padEnd(10)} \${why}\`);
}`;

var app = builder.Build();

// One report job. A real server would run it in the background.
// Here each status check moves it one step, so the output is always the same.
var steps = new[] { "queued", "running", "done" };
var step = -1;

// Building the report takes minutes, so do not make the client wait.
// Say "202 Accepted" and point to a job the client can check.
app.MapPost("/reports", (HttpContext ctx) =>
{
    step = 0;
    ctx.Response.Headers["Location"] = "/jobs/1";
    return Results.Json(new { id = 1, status = steps[step] }, statusCode: 202);
});

app.MapGet("/jobs/{id:int}", (int id, HttpContext ctx) =>
{
    if (step < 0) return Results.NotFound();
    step = Math.Min(step + 1, 2);
    if (steps[step] == "done")
        return Results.Json(new { id, status = "done", result = "/reports/1" });
    // Not ready yet: tell the client how long to wait before asking again.
    ctx.Response.Headers["Retry-After"] = "5";
    return Results.Json(new { id, status = steps[step] });
});

app.MapGet("/reports/{id:int}", (int id) =>
    step == 2 ? Results.Json(new { id, day = "2026-10-15", orders = 42, revenue = 8640 }) : Results.NotFound());

app.Run();`;

// One report job. A real server would run it in the background.
// Here each status check moves it one step, so the output is always the same.
const steps = ["queued", "running", "done"];
let step = -1;

const app = new Hono();

// Building the report takes minutes, so do not make the client wait.
// Say "202 Accepted" and point to a job the client can check.
app.post("/reports", (c) => {
  step = 0;
  c.header("Location", "/jobs/1");
  return c.json({ id: 1, status: steps[step] }, 202);
});

app.get("/jobs/:id", (c) => {
  const id = Number(c.req.param("id"));
  if (step < 0) return c.body(null, 404);
  step = Math.min(step + 1, 2);
  if (steps[step] === "done") return c.json({ id, status: "done", result: "/reports/1" }, 200);
  // Not ready yet: tell the client how long to wait before asking again.
  c.header("Retry-After", "5");
  return c.json({ id, status: steps[step] }, 200);
});

app.get("/reports/:id", (c) => {
  const id = Number(c.req.param("id"));
  return step === 2 ? c.json({ id, day: "2026-10-15", orders: 42, revenue: 8640 }, 200) : c.body(null, 404);
});

###
GET /jobs/1

###
GET /jobs/1

###
GET /reports/1`;

  {
    prompt: "Right now, every job gets REST. Change Choose so that it returns Webhook, gRPC or GraphQL when the job needs it, and REST otherwise.",
    hint: "Check NotifyServer first, then Internal, then ManyShapes. Return REST at the end.",
    explain: "Put the most specific need first, and let REST be what is left. That way REST stays the default, and every other style needs a clear reason.",
    csharp: { starter: `// Pick an API style for each job. REST is the default.
var jobs = new List<Job>
{
    new("Admin menu CRUD"),
    new("Mobile home screen", ManyShapes: true),
    new("Internal stock service", Internal: true),
    new("Tell partner: order shipped", NotifyServer: true),
};
foreach (var job in jobs) Console.WriteLine($"{job.Name,-30} {Choose(job)}");

// TODO: return "Webhook" when NotifyServer, "gRPC" when Internal,
// "GraphQL" when ManyShapes, and "REST" otherwise. Right now everything is REST.
static string Choose(Job j)
{
    return "REST";
}

record Job(string Name, bool Internal = false, bool ManyShapes = false, bool NotifyServer = false);`, solution: `// Pick an API style for each job. REST is the default.
var jobs = new List<Job>
{
    new("Admin menu CRUD"),
    new("Mobile home screen", ManyShapes: true),
    new("Internal stock service", Internal: true),
    new("Tell partner: order shipped", NotifyServer: true),
};
foreach (var job in jobs) Console.WriteLine($"{job.Name,-30} {Choose(job)}");

// Most specific need first. REST is what is left.
static string Choose(Job j)
{
    if (j.NotifyServer) return "Webhook";
    if (j.Internal) return "gRPC";
    if (j.ManyShapes) return "GraphQL";
    return "REST";
}

record Job(string Name, bool Internal = false, bool ManyShapes = false, bool NotifyServer = false);`, mode: "console" },
    react: { starter: `// Pick an API style for each job. REST is the default.
type Job = { name: string; internal?: boolean; manyShapes?: boolean; notifyServer?: boolean };
const jobs: Job[] = [
  { name: "Admin menu CRUD" },
  { name: "Mobile home screen", manyShapes: true },
  { name: "Internal stock service", internal: true },
  { name: "Tell partner: order shipped", notifyServer: true },
];

// TODO: return "Webhook" when notifyServer, "gRPC" when internal,
// "GraphQL" when manyShapes, and "REST" otherwise. Right now everything is REST.
function choose(j: Job): string {
  return "REST";
}

for (const job of jobs) console.log(\`\${job.name.padEnd(30)} \${choose(job)}\`);`, solution: `// Pick an API style for each job. REST is the default.
type Job = { name: string; internal?: boolean; manyShapes?: boolean; notifyServer?: boolean };
const jobs: Job[] = [
  { name: "Admin menu CRUD" },
  { name: "Mobile home screen", manyShapes: true },
  { name: "Internal stock service", internal: true },
  { name: "Tell partner: order shipped", notifyServer: true },
];

// Most specific need first. REST is what is left.
function choose(j: Job): string {
  if (j.notifyServer) return "Webhook";
  if (j.internal) return "gRPC";
  if (j.manyShapes) return "GraphQL";
  return "REST";
}

for (const job of jobs) console.log(\`\${job.name.padEnd(30)} \${choose(job)}\`);`, mode: "console" }
  },
  {
    prompt: "The webhook receiver trusts every request. Make Verify check the X-Signature value, so that a changed body or a made-up signature is rejected.",
    hint: "Sign the body again with the same secret, add sha256= in front, and compare in fixed time (FixedTimeEquals in C#, crypto.subtle.verify in TypeScript).",
    explain: "Only someone who knows the secret can make a signature that fits the body. If you change one byte of the body, the signature no longer fits. A fixed-time compare stops attackers from guessing the signature byte by byte.",
    csharp: { starter: `using System.Security.Cryptography;
using System.Text;

byte[] secret = Encoding.UTF8.GetBytes("whsec_demo_brew_and_bytes");
string body = """{"event":"order.shipped","orderId":101,"status":"shipped"}""";
string good = "sha256=" + Convert.ToHexString(HMACSHA256.HashData(secret, Encoding.UTF8.GetBytes(body))).ToLower();

Console.WriteLine("real body, real signature:  " + Show(Verify(body, good)));
Console.WriteLine("changed body:               " + Show(Verify(body.Replace("101", "102"), good)));
Console.WriteLine("made-up signature:          " + Show(Verify(body, "sha256=" + new string('0', 64))));

// TODO: a webhook receiver must check the X-Signature header.
// Sign "body" with the secret (HMAC-SHA256, lowercase hex, "sha256=" in front)
// and compare it with "header" using CryptographicOperations.FixedTimeEquals.
// Right now it trusts every request.
bool Verify(string body, string header) => true;

// Print true/false in lowercase, like JavaScript does.
static string Show(bool ok) => ok ? "true" : "false";`, solution: `using System.Security.Cryptography;
using System.Text;

byte[] secret = Encoding.UTF8.GetBytes("whsec_demo_brew_and_bytes");
string body = """{"event":"order.shipped","orderId":101,"status":"shipped"}""";
string good = "sha256=" + Convert.ToHexString(HMACSHA256.HashData(secret, Encoding.UTF8.GetBytes(body))).ToLower();

Console.WriteLine("real body, real signature:  " + Show(Verify(body, good)));
Console.WriteLine("changed body:               " + Show(Verify(body.Replace("101", "102"), good)));
Console.WriteLine("made-up signature:          " + Show(Verify(body, "sha256=" + new string('0', 64))));

// Sign the body again, then compare in fixed time.
bool Verify(string body, string header)
{
    string expected = "sha256=" + Convert.ToHexString(HMACSHA256.HashData(secret, Encoding.UTF8.GetBytes(body))).ToLower();
    return CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(header));
}

// Print true/false in lowercase, like JavaScript does.
static string Show(bool ok) => ok ? "true" : "false";`, mode: "console" },
    react: { starter: `const enc = new TextEncoder();
const key = await crypto.subtle.importKey(
  "raw", enc.encode("whsec_demo_brew_and_bytes"), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
const body = \`{"event":"order.shipped","orderId":101,"status":"shipped"}\`;
const hex = (b: ArrayBuffer) => [...new Uint8Array(b)].map((x) => x.toString(16).padStart(2, "0")).join("");
const good = "sha256=" + hex(await crypto.subtle.sign("HMAC", key, enc.encode(body)));

// TODO: a webhook receiver must check the X-Signature header.
// Sign "body" with the key (HMAC-SHA256, lowercase hex, "sha256=" in front)
// and compare it with "header". Right now it trusts every request.
async function verify(body: string, header: string): Promise<boolean> {
  return true;
}

console.log("real body, real signature:  " + (await verify(body, good)));
console.log("changed body:               " + (await verify(body.replace("101", "102"), good)));
console.log("made-up signature:          " + (await verify(body, "sha256=" + "0".repeat(64))));`, solution: `const enc = new TextEncoder();
const key = await crypto.subtle.importKey(
  "raw", enc.encode("whsec_demo_brew_and_bytes"), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
const body = \`{"event":"order.shipped","orderId":101,"status":"shipped"}\`;
const hex = (b: ArrayBuffer) => [...new Uint8Array(b)].map((x) => x.toString(16).padStart(2, "0")).join("");
const good = "sha256=" + hex(await crypto.subtle.sign("HMAC", key, enc.encode(body)));

// Check the prefix, turn the hex back into bytes, and let crypto.subtle.verify
// sign the body again and compare in fixed time.
async function verify(body: string, header: string): Promise<boolean> {
  if (!header.startsWith("sha256=")) return false;
  const sig = new Uint8Array(header.slice(7).match(/../g)!.map((h) => parseInt(h, 16)));
  return crypto.subtle.verify("HMAC", key, sig, enc.encode(body));
}

console.log("real body, real signature:  " + (await verify(body, good)));
console.log("changed body:               " + (await verify(body.replace("101", "102"), good)));
console.log("made-up signature:          " + (await verify(body, "sha256=" + "0".repeat(64))));`, mode: "console" }
  },
  {
    prompt: "Partners should register a webhook URL instead of polling. Return 201 Created with a Location header. Reject URLs that do not start with https:// with a 400 validation problem.",
    hint: "Use Results.ValidationProblem with the key url, and Results.Created with /webhooks/{id}. In Hono, send application/problem+json and set the Location header.",
    explain: "Registering a webhook creates a resource, so it gets 201 and a Location. Plain http would let anyone on the network read the events, so the API refuses it with the same validation shape as every other 400.",
    csharp: { starter: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var hooks = new List<Hook>();

// Partners stop polling and register a URL instead. We call it when an order ships.
// TODO: 1) if the URL does not start with "https://", return Results.ValidationProblem
//          with the key "url" and the message "The URL must start with https://."
//       2) on success return 201 Created with Location: /webhooks/{id}
app.MapPost("/webhooks", (NewHook input) =>
{
    var hook = new Hook(hooks.Count + 1, input.Url, input.Events);
    hooks.Add(hook);
    return Results.Ok(hook);
});

app.Run();

record NewHook(string Url, string[] Events);
record Hook(int Id, string Url, string[] Events);`, solution: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var hooks = new List<Hook>();

// Partners stop polling and register a URL instead. We call it when an order ships.
app.MapPost("/webhooks", (NewHook input) =>
{
    // Signed payloads must travel over https, or anyone on the way can read them.
    if (!input.Url.StartsWith("https://"))
    {
        return Results.ValidationProblem(new Dictionary<string, string[]>
        {
            ["url"] = new[] { "The URL must start with https://." }
        });
    }
    var hook = new Hook(hooks.Count + 1, input.Url, input.Events);
    hooks.Add(hook);
    return Results.Created($"/webhooks/{hook.Id}", hook);
});

app.Run();

record NewHook(string Url, string[] Events);
record Hook(int Id, string Url, string[] Events);`, mode: "aspnet", requests: `POST /webhooks
Content-Type: application/json

{"url":"https://partner.example.com/hooks","events":["order.shipped"]}

###
POST /webhooks
Content-Type: application/json

{"url":"http://partner.example.com/hooks","events":["order.shipped"]}` },
    react: { starter: `import { Hono } from "hono";

type Hook = { id: number; url: string; events: string[] };
const hooks: Hook[] = [];

const app = new Hono();

// Partners stop polling and register a URL instead. We call it when an order ships.
// TODO: 1) if the URL does not start with "https://", return 400 problem+json with
//          errors: { url: ["The URL must start with https://."] }
//       2) on success return 201 Created with Location: /webhooks/{id}
app.post("/webhooks", async (c) => {
  const input = await c.req.json<{ url: string; events: string[] }>();
  const hook = { id: hooks.length + 1, url: input.url, events: input.events };
  hooks.push(hook);
  return c.json(hook, 200);
});

type Hook = { id: number; url: string; events: string[] };
const hooks: Hook[] = [];

const app = new Hono();

// Partners stop polling and register a URL instead. We call it when an order ships.
app.post("/webhooks", async (c) => {
  const input = await c.req.json<{ url: string; events: string[] }>();
  // Signed payloads must travel over https, or anyone on the way can read them.
  if (!input.url.startsWith("https://")) {
    const problem = {
      type: "https://tools.ietf.org/html/rfc9110#section-15.5.1",
      title: "One or more validation errors occurred.",
      status: 400,
      errors: { url: ["The URL must start with https://."] },
    };
    return c.body(JSON.stringify(problem), 400, { "Content-Type": "application/problem+json" });
  }
  const hook = { id: hooks.length + 1, url: input.url, events: input.events };
  hooks.push(hook);
  c.header("Location", \`/webhooks/\${hook.id}\`);
  return c.json(hook, 201);
});

Content-Type: application/json

{"url":"https://partner.example.com/hooks","events":["order.shipped"]}

###
POST /webhooks
Content-Type: application/json

{"url":"http://partner.example.com/hooks","events":["order.shipped"]}` },
    checks: `POST /webhooks
Content-Type: application/json

{"url":"https://shop.example.org/cb","events":["order.paid"]}

###
POST /webhooks
Content-Type: application/json

{"url":"ftp://shop.example.org/cb","events":["order.paid"]}

###
POST /webhooks
Content-Type: application/json

{"url":"https://b.example.com/h","events":["order.shipped","order.paid"]}`
  }
];
