# HTTP Methods

Course: [API](https://www.devtutorials.ai/learn/building-apis) · Section: REST Foundations
Source: https://www.devtutorials.ai/learn/building-apis/rest-foundations/http-methods

> Learn GET, POST, PUT, PATCH and DELETE, what safe and idempotent mean, and how an Idempotency-Key stops a retried payment from charging twice.

An **HTTP method** is a command that tells the server what the client wants to do with a resource. Every HTTP request starts with a method. The method explains the purpose of the request, such as reading data, creating new data, updating existing data, or deleting data. For example, `GET` is used to read data, `POST` is used to create data, `PUT` is used to replace data, `PATCH` is used to update part of the data, and `DELETE` is used to remove data. Using the correct method makes APIs easier to understand and helps clients and servers communicate clearly.

## Watch a Customer Pay Twice

Asha orders two lattes and taps **Pay**. The network is slow. The app waits for a response, gives up, and sends the same payment request again. Press **Run**. The Requests box contains the original payment request and its retry.

Look at the final response. The server has created two payments of ₹440. Asha was charged twice for the same order.

**In simple words:** the server cannot tell whether the second request is a retry or a brand-new payment. Later in this lesson, you will learn how to fix this using an **Idempotency-Key**.

## TL;DR

- **GET** reads data.
- **POST** creates new data.
- **PUT** replaces existing data.
- **PATCH** updates part of existing data.
- **DELETE** removes data.
- A **safe** method does not change anything on the server.
- An **idempotent** method has the same result whether it is sent once or many times.
- GET, PUT, and DELETE are idempotent.
- POST is not idempotent.
- Network problems can cause requests to be retried.
- Retrying a POST can accidentally create duplicate orders or payments.
- An **Idempotency-Key** makes POST requests safe to retry.
- If a path exists but the HTTP method is not allowed, the server returns `405 Method Not Allowed`.

## A Real-World Example

Think about pressing a lift button. You press the button for floor 3. The lift is slow, so you press the button again. You may even press it several more times. The lift still goes to floor 3 only once. This is called **idempotent** behavior. Whether you press the button once or many times, the result stays the same. Now think about a coffee machine. 

Every time you press the **Make Coffee** button, a new cup of coffee is prepared. Press it twice and you get two cups. This is **not idempotent** because every press creates something new. A POST request works in a similar way. Each request usually creates a new resource. That is why retrying a POST can be risky.

## What You Will Learn

In this lesson, you will learn:

1. The five common HTTP methods and when to use them.
2. What **safe** and **idempotent** mean.
3. Why a retry can create duplicate payments.
4. How an **Idempotency-Key** makes POST requests safe to retry.
5. Why using GET to change data is a serious mistake.

## The Five Methods

One resource can support different HTTP methods. In this example, the resource is an order. Press **Run** and review the responses.

| Method | Purpose | Example Response | Request Body |
|----------|----------|----------|----------|
| `GET /orders/101` | Read order 101 | `200 OK` and the order | No |
| `POST /orders` | Create a new order | `201 Created`, a `Location` header, and the order | Yes |
| `PUT /orders/102` | Replace order 102 completely | `200 OK` and the updated order | Yes, complete object |
| `PATCH /orders/102` | Update part of order 102 | `200 OK` and the updated order | Yes, changed fields only |
| `DELETE /orders/102` | Delete order 102 | `204 No Content` | No |

The final request reads order 102 again and receives `404 Not Found` because the order was deleted.

### PUT vs PATCH

Both methods modify existing data, but they work differently.

**PUT**

- Replaces the entire resource.
- The client sends all fields.
- Missing fields may be removed or overwritten.

**PATCH**

- Updates only specific fields.
- The client sends only the fields that change.
- Useful for small updates such as changing a status.

In C#, `app.MapGet`, `MapPost`, `MapPut`, `MapPatch` and `MapDelete` connect a method and a path to your code. In Hono, the same job is done by `app.get`, `app.post`, `app.put`, `app.patch` and `app.delete`.

## Safe and Idempotent

These two concepts are important when working with HTTP methods.

### Safe

A method is **safe** when it does not change anything on the server. Safe methods only read data. For example: GET /menu This request reads information but does not create, update, or delete anything.

### Idempotent

A method is **idempotent** when sending it multiple times results in the same final state as sending it once. For example:

- Updating the same item with the same data multiple times produces the same result.
- Deleting an item multiple times still leaves the item deleted.

| Method | Safe? | Idempotent? |
|----------|----------|----------|
| GET | Yes | Yes |
| PUT | No | Yes |
| DELETE | No | Yes |
| PATCH | No | Not always |
| POST | No | No |

### See Idempotent in Action

The next example sends the same PUT twice and then the same DELETE twice.

Both PUT requests produce the same result. The second request does not create any additional changes.

That makes PUT idempotent. The first DELETE removes the order and returns `204 No Content`. The second DELETE finds nothing and returns `404 Not Found`. Although the responses are different, the final state is still the same: the order does not exist. That is why DELETE is also considered idempotent.

## Make a Retry Safe

POST is not idempotent. If a client retries a POST request, the server may create another order or process another payment. To prevent this problem, clients can send an **Idempotency-Key** header. The key should be unique for each real operation. When the same key is received again, the server knows that it is a retry and returns the original result instead of creating a new one.

The retry uses the same key: order-101-pay-1
The server recognizes it as the same payment request and returns the first payment instead of processing another charge. As a result, Asha pays only once.

The client makes the key, not the server. Make one new key for each real payment, and reuse it only for retries of that payment. Big payment APIs work exactly this way.

## The Wrong Method

Sometimes a path exists but does not support the method being used. In that case, the server responds with: 405 Method Not Allowed
The response also includes an `Allow` header that lists the supported methods.

In ASP.NET Core, `405 Method Not Allowed` and the `Allow` header are usually handled automatically. In Hono, you may need to configure this behavior yourself.

**Never change data with GET.** Browsers, link previews and search robots open GET links on their own. If `GET /orders/101/cancel` cancels an order, a chat app that shows a preview of the link can cancel it. Use POST, PUT, PATCH or DELETE for anything that changes data.

## Where You See This

- **Payment APIs** use Idempotency-Key headers to prevent duplicate charges.
- **Mobile apps** retry requests when the network is unreliable.
- **Browsers** sometimes preload or preview GET requests automatically.
- **Order systems** use different HTTP methods to create, update, and remove data.

## Common mistakes

| Problem | Cause | Solution |
|---|---|---|
| Customer is charged twice | POST request was retried and processed twice | Use an Idempotency-Key and validate it on the server |
| Orders are cancelled unexpectedly | A GET request changes data and was triggered automatically | Replace GET with POST, PUT, PATCH, or DELETE |
| `405 Method Not Allowed` | The path exists but the wrong method was used | Check the `Allow` header and use one of the supported methods |
| Data disappears after a PUT | PUT replaces the entire resource and not all fields were sent | Send all fields with PUT or use PATCH instead |

## Exercises

## What to Remember

- **GET** reads, **POST** creates, **PUT** replaces, **PATCH** changes part, **DELETE** removes.
- **Safe** means that it changes nothing. **Idempotent** means that repeats do no extra harm.
- POST is not idempotent, so a retry can make a second order or payment.
- An **Idempotency-Key** header makes a POST safe to retry.
- Never change data with GET.

**Next:** REST API Design and URL Naming. You will turn action-style URLs into clean resource URLs, and you will add filtering, sorting and pages.

var app = builder.Build();

var orders = new Dictionary<int, Order>
{
    [101] = new(101, "Latte", 1, "received")
};

// GET reads. It changes nothing.
app.MapGet("/orders/{id:int}", (int id) =>
    orders.TryGetValue(id, out var order) ? Results.Ok(order) : Results.NotFound());

// POST creates something new.
app.MapPost("/orders", (NewOrder input) =>
{
    var order = new Order(orders.Keys.Max() + 1, input.Item, input.Quantity, "received");
    orders[order.Id] = order;
    return Results.Created("/orders/" + order.Id, order);
});

// PUT replaces the whole order with what you send.
app.MapPut("/orders/{id:int}", (int id, NewOrder input) =>
{
    if (!orders.ContainsKey(id)) return Results.NotFound();
    orders[id] = new Order(id, input.Item, input.Quantity, orders[id].Status);
    return Results.Ok(orders[id]);
});

// PATCH changes one part: here, only the status.
app.MapPatch("/orders/{id:int}", (int id, StatusChange change) =>
{
    if (!orders.ContainsKey(id)) return Results.NotFound();
    orders[id] = orders[id] with { Status = change.Status };
    return Results.Ok(orders[id]);
});

// DELETE removes it.
app.MapDelete("/orders/{id:int}", (int id) =>
    orders.Remove(id) ? Results.NoContent() : Results.NotFound());

app.Run();

record NewOrder(string Item, int Quantity);
record StatusChange(string Status);
record Order(int Id, string Item, int Quantity, string Status);`;

type NewOrder = { item: string; quantity: number };
type Order = { id: number; item: string; quantity: number; status: string };

const orders = new Map<number, Order>([[101, { id: 101, item: "Latte", quantity: 1, status: "received" }]]);
const app = new Hono();

// GET reads. It changes nothing.
app.get("/orders/:id", (c) => {
  const order = orders.get(Number(c.req.param("id")));
  return order ? c.json(order, 200) : c.body(null, 404);
});

// POST creates something new.
app.post("/orders", async (c) => {
  const input = await c.req.json<NewOrder>();
  const order: Order = { id: Math.max(...orders.keys()) + 1, item: input.item, quantity: input.quantity, status: "received" };
  orders.set(order.id, order);
  c.header("Location", "/orders/" + order.id);
  return c.json(order, 201);
});

// PUT replaces the whole order with what you send.
app.put("/orders/:id", async (c) => {
  const id = Number(c.req.param("id"));
  const old = orders.get(id);
  if (!old) return c.body(null, 404);
  const input = await c.req.json<NewOrder>();
  const order: Order = { id, item: input.item, quantity: input.quantity, status: old.status };
  orders.set(id, order);
  return c.json(order, 200);
});

// PATCH changes one part: here, only the status.
app.patch("/orders/:id", async (c) => {
  const id = Number(c.req.param("id"));
  const old = orders.get(id);
  if (!old) return c.body(null, 404);
  const change = await c.req.json<{ status: string }>();
  const order: Order = { ...old, status: change.status };
  orders.set(id, order);
  return c.json(order, 200);
});

// DELETE removes it.
app.delete("/orders/:id", (c) => (orders.delete(Number(c.req.param("id"))) ? c.body(null, 204) : c.body(null, 404)));

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

{"item":"Masala Chai","quantity":2}

###
PUT /orders/102
Content-Type: application/json

{"item":"Masala Chai","quantity":3}

###
PATCH /orders/102
Content-Type: application/json

{"status":"ready"}

###
DELETE /orders/102

###
GET /orders/102`;

var app = builder.Build();

var payments = new List<Payment>();

// Every POST creates a new payment. Even if it is the same payment, sent twice.
app.MapPost("/payments", (NewPayment input) =>
{
    var payment = new Payment(payments.Count + 1, input.OrderId, input.Amount);
    payments.Add(payment);
    return TypedResults.Created("/payments/" + payment.Id, payment);
});

app.MapGet("/payments", () => TypedResults.Ok(payments));

app.Run();

record NewPayment(int OrderId, int Amount);
record Payment(int Id, int OrderId, int Amount);`;

type NewPayment = { orderId: number; amount: number };
type Payment = { id: number; orderId: number; amount: number };

const payments: Payment[] = [];
const app = new Hono();

// Every POST creates a new payment. Even if it is the same payment, sent twice.
app.post("/payments", async (c) => {
  const input = await c.req.json<NewPayment>();
  const payment: Payment = { id: payments.length + 1, orderId: input.orderId, amount: input.amount };
  payments.push(payment);
  c.header("Location", "/payments/" + payment.id);
  return c.json(payment, 201);
});

app.get("/payments", (c) => c.json(payments, 200));

Content-Type: application/json

{"orderId":101,"amount":440}

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

{"orderId":101,"amount":440}

###
GET /payments`;

var app = builder.Build();

var payments = new List<Payment>();
var seen = new Dictionary<string, Payment>();   // Idempotency-Key -> the payment it made

app.MapPost("/payments", (HttpRequest request, NewPayment input) =>
{
    string key = request.Headers["Idempotency-Key"].ToString();

    // Seen this key before? Send back the same payment. Do not charge again.
    if (key != "" && seen.TryGetValue(key, out var earlier))
    {
        return TypedResults.Created("/payments/" + earlier.Id, earlier);
    }

    var payment = new Payment(payments.Count + 1, input.OrderId, input.Amount);
    payments.Add(payment);
    if (key != "") seen[key] = payment;
    return TypedResults.Created("/payments/" + payment.Id, payment);
});

app.MapGet("/payments", () => TypedResults.Ok(payments));

app.Run();

record NewPayment(int OrderId, int Amount);
record Payment(int Id, int OrderId, int Amount);`;

type NewPayment = { orderId: number; amount: number };
type Payment = { id: number; orderId: number; amount: number };

const payments: Payment[] = [];
const seen = new Map<string, Payment>(); // Idempotency-Key -> the payment it made
const app = new Hono();

app.post("/payments", async (c) => {
  const key = c.req.header("Idempotency-Key") ?? "";

  // Seen this key before? Send back the same payment. Do not charge again.
  const earlier = key ? seen.get(key) : undefined;
  if (earlier) {
    c.header("Location", "/payments/" + earlier.id);
    return c.json(earlier, 201);
  }

  const input = await c.req.json<NewPayment>();
  const payment: Payment = { id: payments.length + 1, orderId: input.orderId, amount: input.amount };
  payments.push(payment);
  if (key) seen.set(key, payment);
  c.header("Location", "/payments/" + payment.id);
  return c.json(payment, 201);
});

app.get("/payments", (c) => c.json(payments, 200));

Content-Type: application/json
Idempotency-Key: order-101-pay-1

{"orderId":101,"amount":440}

###
POST /payments
Content-Type: application/json
Idempotency-Key: order-101-pay-1

{"orderId":101,"amount":440}

###
GET /payments`;

var app = builder.Build();

var orders = new Dictionary<int, Order>
{
    [101] = new(101, "Latte", 1)
};

// PUT twice with the same body: the second one changes nothing new.
app.MapPut("/orders/{id:int}", (int id, Order input) =>
{
    orders[id] = input with { Id = id };
    return TypedResults.Ok(orders[id]);
});

// DELETE twice: the first removes it, the second finds nothing.
app.MapDelete("/orders/{id:int}", (int id) =>
    orders.Remove(id) ? Results.NoContent() : Results.NotFound());

app.Run();

record Order(int Id, string Item, int Quantity);`;

type Order = { id: number; item: string; quantity: number };

const orders = new Map<number, Order>([[101, { id: 101, item: "Latte", quantity: 1 }]]);
const app = new Hono();

// PUT twice with the same body: the second one changes nothing new.
app.put("/orders/:id", async (c) => {
  const id = Number(c.req.param("id"));
  const input = await c.req.json<Order>();
  const order: Order = { id, item: input.item, quantity: input.quantity };
  orders.set(id, order);
  return c.json(order, 200);
});

// DELETE twice: the first removes it, the second finds nothing.
app.delete("/orders/:id", (c) => (orders.delete(Number(c.req.param("id"))) ? c.body(null, 204) : c.body(null, 404)));

Content-Type: application/json

{"item":"Latte","quantity":2}

###
PUT /orders/101
Content-Type: application/json

{"item":"Latte","quantity":2}

###
DELETE /orders/101

###
DELETE /orders/101`;

var app = builder.Build();

var menu = new List<MenuItem> { new(1, "Latte", 220) };

// The menu can only be read.
app.MapGet("/menu", () => TypedResults.Ok(menu));

app.Run();

record MenuItem(int Id, string Name, int Price);`;

type MenuItem = { id: number; name: string; price: number };
const menu: MenuItem[] = [{ id: 1, name: "Latte", price: 220 }];

const app = new Hono();

// The menu can only be read.
app.get("/menu", (c) => c.json(menu, 200));

// Hono does not answer 405 by itself. This line tells clients which methods work here.
app.all("/menu", (c) => {
  c.header("Allow", "GET");
  return c.body(null, 405);
});

###
DELETE /menu`;

  {
    prompt: "Return the right HTTP method for each of the five actions.",
    hint: "Read: GET. Create: POST. Replace: PUT. Change one part: PATCH. Remove: DELETE.",
    explain: "The method is the verb of a request, so the URL never needs one.",
    csharp: { starter: `// Return the HTTP method for each action.
static string MethodFor(string action)
{
    return "GET";
}

Console.WriteLine("See the menu: " + MethodFor("see the menu"));
Console.WriteLine("Place an order: " + MethodFor("place an order"));
Console.WriteLine("Replace an order: " + MethodFor("replace an order"));
Console.WriteLine("Change only the status: " + MethodFor("change only the status"));
Console.WriteLine("Cancel and remove an order: " + MethodFor("cancel and remove an order"));`, solution: `// Return the HTTP method for each action.
static string MethodFor(string action)
{
    return action switch
    {
        "see the menu" => "GET",
        "place an order" => "POST",
        "replace an order" => "PUT",
        "change only the status" => "PATCH",
        "cancel and remove an order" => "DELETE",
        _ => "?"
    };
}

Console.WriteLine("See the menu: " + MethodFor("see the menu"));
Console.WriteLine("Place an order: " + MethodFor("place an order"));
Console.WriteLine("Replace an order: " + MethodFor("replace an order"));
Console.WriteLine("Change only the status: " + MethodFor("change only the status"));
Console.WriteLine("Cancel and remove an order: " + MethodFor("cancel and remove an order"));`, mode: "console" },
    react: { starter: `// Return the HTTP method for each action.
function methodFor(action: string): string {
  return "GET";
}

console.log("See the menu: " + methodFor("see the menu"));
console.log("Place an order: " + methodFor("place an order"));
console.log("Replace an order: " + methodFor("replace an order"));
console.log("Change only the status: " + methodFor("change only the status"));
console.log("Cancel and remove an order: " + methodFor("cancel and remove an order"));`, solution: `// Return the HTTP method for each action.
function methodFor(action: string): string {
  const methods: Record<string, string> = {
    "see the menu": "GET",
    "place an order": "POST",
    "replace an order": "PUT",
    "change only the status": "PATCH",
    "cancel and remove an order": "DELETE",
  };
  return methods[action] ?? "?";
}

console.log("See the menu: " + methodFor("see the menu"));
console.log("Place an order: " + methodFor("place an order"));
console.log("Replace an order: " + methodFor("replace an order"));
console.log("Change only the status: " + methodFor("change only the status"));
console.log("Cancel and remove an order: " + methodFor("cancel and remove an order"));`, mode: "console" }
  },
  {
    prompt: "The app retries a payment with the same Idempotency-Key. Make the server return the first payment instead of charging again. The last request should list only one payment.",
    hint: "Keep a dictionary from key to payment. Before you create a payment, check whether the key is already in it.",
    explain: "The key turns a non-idempotent POST into a safe retry: the same key always gets the same result.",
    csharp: { starter: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var payments = new List<Payment>();

// The app retries when the network is slow. Make a retry with the same
// Idempotency-Key send back the first payment instead of charging again.
app.MapPost("/payments", (NewPayment input) =>
{
    var payment = new Payment(payments.Count + 1, input.OrderId, input.Amount);
    payments.Add(payment);
    return TypedResults.Created("/payments/" + payment.Id, payment);
});

app.MapGet("/payments", () => TypedResults.Ok(payments));

app.Run();

record NewPayment(int OrderId, int Amount);
record Payment(int Id, int OrderId, int Amount);`, solution: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var payments = new List<Payment>();
var seen = new Dictionary<string, Payment>();   // Idempotency-Key -> the payment it made

app.MapPost("/payments", (HttpRequest request, NewPayment input) =>
{
    string key = request.Headers["Idempotency-Key"].ToString();

    // Seen this key before? Send back the same payment. Do not charge again.
    if (key != "" && seen.TryGetValue(key, out var earlier))
    {
        return TypedResults.Created("/payments/" + earlier.Id, earlier);
    }

    var payment = new Payment(payments.Count + 1, input.OrderId, input.Amount);
    payments.Add(payment);
    if (key != "") seen[key] = payment;
    return TypedResults.Created("/payments/" + payment.Id, payment);
});

app.MapGet("/payments", () => TypedResults.Ok(payments));

app.Run();

record NewPayment(int OrderId, int Amount);
record Payment(int Id, int OrderId, int Amount);`, mode: "aspnet", requests: `POST /payments
Content-Type: application/json
Idempotency-Key: order-101-pay-1

{"orderId":101,"amount":440}

###
POST /payments
Content-Type: application/json
Idempotency-Key: order-101-pay-1

{"orderId":101,"amount":440}

###
GET /payments` },
    react: { starter: `import { Hono } from "hono";

type NewPayment = { orderId: number; amount: number };
type Payment = { id: number; orderId: number; amount: number };

const payments: Payment[] = [];
const app = new Hono();

// The app retries when the network is slow. Make a retry with the same
// Idempotency-Key send back the first payment instead of charging again.
app.post("/payments", async (c) => {
  const input = await c.req.json<NewPayment>();
  const payment: Payment = { id: payments.length + 1, orderId: input.orderId, amount: input.amount };
  payments.push(payment);
  c.header("Location", "/payments/" + payment.id);
  return c.json(payment, 201);
});

app.get("/payments", (c) => c.json(payments, 200));

type NewPayment = { orderId: number; amount: number };
type Payment = { id: number; orderId: number; amount: number };

const payments: Payment[] = [];
const seen = new Map<string, Payment>(); // Idempotency-Key -> the payment it made
const app = new Hono();

app.post("/payments", async (c) => {
  const key = c.req.header("Idempotency-Key") ?? "";

  // Seen this key before? Send back the same payment. Do not charge again.
  const earlier = key ? seen.get(key) : undefined;
  if (earlier) {
    c.header("Location", "/payments/" + earlier.id);
    return c.json(earlier, 201);
  }

  const input = await c.req.json<NewPayment>();
  const payment: Payment = { id: payments.length + 1, orderId: input.orderId, amount: input.amount };
  payments.push(payment);
  if (key) seen.set(key, payment);
  c.header("Location", "/payments/" + payment.id);
  return c.json(payment, 201);
});

app.get("/payments", (c) => c.json(payments, 200));

Content-Type: application/json
Idempotency-Key: order-101-pay-1

{"orderId":101,"amount":440}

###
POST /payments
Content-Type: application/json
Idempotency-Key: order-101-pay-1

{"orderId":101,"amount":440}

###
GET /payments` },
    checks: `POST /payments
Content-Type: application/json
Idempotency-Key: order-102-pay-1

{"orderId":102,"amount":240}

###
POST /payments
Content-Type: application/json
Idempotency-Key: order-102-pay-1

{"orderId":102,"amount":240}

###
GET /payments`
  },
  {
    prompt: "Opening GET /orders/101/cancel cancels the order, so a link preview cancelled it. Make cancel a POST, so that a GET can no longer change the order.",
    hint: "Change only the method of the cancel route. In C#, MapGet becomes MapPost. In Hono, app.get becomes app.post.",
    explain: "GET must be safe. After the change, the GET gets 405 (ASP.NET Core) or 404 (Hono), and the order stays received.",
    csharp: { starter: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var orders = new Dictionary<int, Order> { [101] = new(101, "Latte", "received") };

app.MapGet("/orders/{id:int}", (int id) => TypedResults.Ok(orders[id]));

// A link-preview robot opened this address, and the order got cancelled.
// Cancelling changes the order, so it must not be a GET. Make it a POST.
app.MapGet("/orders/{id:int}/cancel", (int id) =>
{
    orders[id] = orders[id] with { Status = "cancelled" };
    return TypedResults.Ok(orders[id]);
});

app.Run();

record Order(int Id, string Item, string Status);`, solution: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var orders = new Dictionary<int, Order> { [101] = new(101, "Latte", "received") };

app.MapGet("/orders/{id:int}", (int id) => TypedResults.Ok(orders[id]));

// A link-preview robot opened this address, and the order got cancelled.
// Cancelling changes the order, so it must not be a GET. Make it a POST.
app.MapPost("/orders/{id:int}/cancel", (int id) =>
{
    orders[id] = orders[id] with { Status = "cancelled" };
    return TypedResults.Ok(orders[id]);
});

app.Run();

record Order(int Id, string Item, string Status);`, mode: "aspnet", requests: `GET /orders/101/cancel

###
GET /orders/101` },
    react: { starter: `import { Hono } from "hono";

type Order = { id: number; item: string; status: string };
const orders = new Map<number, Order>([[101, { id: 101, item: "Latte", status: "received" }]]);

const app = new Hono();

app.get("/orders/:id", (c) => c.json(orders.get(Number(c.req.param("id"))), 200));

// A link-preview robot opened this address, and the order got cancelled.
// Cancelling changes the order, so it must not be a GET. Make it a POST.
app.get("/orders/:id/cancel", (c) => {
  const id = Number(c.req.param("id"));
  const order = { ...orders.get(id)!, status: "cancelled" };
  orders.set(id, order);
  return c.json(order, 200);
});

type Order = { id: number; item: string; status: string };
const orders = new Map<number, Order>([[101, { id: 101, item: "Latte", status: "received" }]]);

const app = new Hono();

app.get("/orders/:id", (c) => c.json(orders.get(Number(c.req.param("id"))), 200));

// A link-preview robot opened this address, and the order got cancelled.
// Cancelling changes the order, so it must not be a GET. Make it a POST.
app.post("/orders/:id/cancel", (c) => {
  const id = Number(c.req.param("id"));
  const order = { ...orders.get(id)!, status: "cancelled" };
  orders.set(id, order);
  return c.json(order, 200);
});

###
GET /orders/101` },
    checks: `POST /orders/101/cancel

###
GET /orders/101

###
GET /orders/101/cancel`
  }
];
