# Building a CRUD API

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

> Build create, read, update and delete endpoints for the menu and orders, guard status changes with 409, and move logic into services.

**CRUD** stands for **Create, Read, Update, and Delete**.

CRUD represents the four most common actions that applications perform when working with data. Whether you are building a menu system, an online store, a contact list, or a banking application, you usually need to create new data, read existing data, update information, and delete data that is no longer needed.

In APIs, CRUD actions are usually mapped to HTTP methods. Clients send requests to create, view, modify, or remove resources, and the server performs the requested action and returns a response.

In this lesson, you will learn how to build all four CRUD operations for the Brew & Bytes menu and orders. By the end, you will understand the basic structure used by most real-world APIs.

## Watch a Menu Change Live

The coffee shop adds a new item called Cold Coffee, changes its price, and removes Masala Chai.

Press **Run** and watch the five responses.

- `POST /menu` creates a new menu item called Cold Coffee. The response is `201 Created` and includes a `Location` header.
- `GET /menu` returns all three menu items.
- `PUT /menu/3` updates the price to ₹190.
- `DELETE /menu/2` removes Masala Chai. The response is `204 No Content`.
- The final `GET /menu` shows the updated menu.

**In simple words:** these five endpoints handle all the common actions for a resource. Most APIs follow this same pattern.

## TL;DR

- **Create** uses `POST /things`. The response is usually `201 Created` with a `Location` header.
- **Read** uses `GET /things` or `GET /things/{id}`. The response is usually `200 OK`, or `404 Not Found` if the item does not exist.
- **Update** uses `PUT` for replacing a complete item or `PATCH` for changing part of an item. The response is usually `200 OK`, or `404 Not Found` if the item does not exist.
- **Delete** uses `DELETE /things/{id}`. The response is usually `204 No Content`, or `404 Not Found` if the item does not exist.
- Use `MapGroup` in C# or a sub-app in Hono to organize related endpoints.
- Keep business rules, such as order status changes, on the server.
- If a request breaks a rule, return `409 Conflict`.
- Move data and business logic into a **service** to keep endpoints simple.

## A Real-World Example

Think about the contacts app on your phone.

You can:

- **Create** a new contact.
- **Read** a contact or view the contact list.
- **Update** a phone number.
- **Delete** an old contact.

These are the same four CRUD actions. Many applications work in exactly the same way, whether they manage contacts, products, orders, or customers.

## What You Will Learn

In this lesson, you will learn:

1. The five CRUD endpoints and the status codes they use.
2. How to organize related endpoints under one path.
3. How to create an order with multiple items and a server-calculated total.
4. How to protect status changes using `409 Conflict`.
5. How to move business logic into a service class.

## The Five Endpoints

`MapGroup` in C# and `new Hono()` plus `app.route("/menu", menu)` in TypeScript do the same job: they put related endpoints under one path. When the API grows, each resource gets its own group.

## Orders with Multiple Items

In a real application, an order can contain several items. Each **order line** contains:

- A menu item.
- A quantity.

The server looks up the item prices, calculates the line totals, and then calculates the final order total.

For example:

- Two lattes cost ₹440.
- One muffin costs ₹150.

The total becomes ₹590. The client only sends the item names and quantities. The server calculates all prices and totals.

## Protect the Order Status

An order moves through these stages:

1. `received`
2. `preparing`
3. `ready`
4. `delivered`

The order must move one step at a time. The client uses `PATCH` to change the status, and the server checks whether the change is allowed.

Examples:

- `received → preparing` is allowed.
- `preparing → delivered` skips the `ready` stage, so the server returns `409 Conflict`.
- `preparing → ready` is allowed.

The `409 Conflict` response includes a message explaining the problem.

**Never trust the client to follow the rules.** If the app hides the "Delivered" button, someone can still send the PATCH directly. The rule must live in the server.

## Services and Endpoints

As an API grows, `Program.cs` can become large and difficult to manage.

A common solution is to split the code into separate parts:

- A **service** class contains the data and business rules, such as `MenuService`.
- An **endpoints** class defines the API routes and calls the service, such as `MenuEndpoints`.
- `Program.cs` connects everything together.

`builder.Services.AddSingleton<MenuService>()` registers the service.

This feature is called **Dependency Injection**.

ASP.NET Core creates one instance of the service and provides it automatically to endpoints that need it.

Use the file tabs to explore each part of the application.

Keep endpoints thin. An endpoint reads the request, calls a service, and picks a status code. The rules live in the service, where they are easy to test without HTTP.

## The Whole App

The final example shows the complete flow:

1. Read the menu.
2. Create an order.
3. Try creating an invalid order.
4. Read the order.
5. Delete the order.
6. Try reading the deleted order.

The invalid order uses a menu id that does not exist, so the API returns a validation error.

After the order is deleted, the final request returns `404 Not Found`.

## Take It Home

Download the finished API from the final example and run it on your own computer.

- The C# version is an ASP.NET Core 10 project.
- The TypeScript version is a Node.js project that uses Hono.
- Both projects include a `.http` file with sample requests.

## Where You See This

CRUD is used almost everywhere:

- **Admin dashboards** let users create, view, edit, and delete records.
- **Shopping carts** add items, update quantities, and remove products.
- **Food delivery apps** update order statuses and prevent invalid changes.

## Common mistakes

| Symptom | Cause | Fix |
|---|---|---|
| Updating a missing item creates it or crashes | There is no `404` check in PUT | Look the item up first, and return `404` if it is missing |
| Delivered orders go back to "preparing" | There is no status rule | Allow only the next step, and otherwise reply `409` |
| Wrong order totals | The total is sent by the client | Compute it from the lines and the prices on the server |
| `Program.cs` is 800 lines long | All the logic is in the endpoints | Split it into services and endpoint classes |

## Exercises

## What to Remember

- CRUD is **POST**, **GET**, **PUT/PATCH** and **DELETE** on one resource.
- Use `201` + `Location` on create, `204` on delete, and `404` for a missing id.
- The server works out totals and enforces state rules (`409`).
- Group endpoints, and move logic into services.

**Next:** API Documentation with OpenAPI. You will describe this API in one file that tools can read.

var app = builder.Build();

var menu = new Dictionary<int, MenuItem>
{
    [1] = new(1, "Latte", 220),
    [2] = new(2, "Masala Chai", 120)
};

var group = app.MapGroup("/menu");

// Create
group.MapPost("/", (MenuItemRequest input) =>
{
    var item = new MenuItem(menu.Keys.Max() + 1, input.Name, input.Price);
    menu[item.Id] = item;
    return Results.Created("/menu/" + item.Id, item);
});

// Read: the list, and one item
group.MapGet("/", () => Results.Ok(menu.Values));
group.MapGet("/{id:int}", (int id) =>
    menu.TryGetValue(id, out var item) ? Results.Ok(item) : Results.NotFound());

// Update
group.MapPut("/{id:int}", (int id, MenuItemRequest input) =>
{
    if (!menu.ContainsKey(id)) return Results.NotFound();
    menu[id] = new MenuItem(id, input.Name, input.Price);
    return Results.Ok(menu[id]);
});

// Delete
group.MapDelete("/{id:int}", (int id) =>
    menu.Remove(id) ? Results.NoContent() : Results.NotFound());

app.Run();

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

type MenuItemRequest = { name: string; price: number };
type MenuItem = { id: number; name: string; price: number };

const menu = new Map<number, MenuItem>([
  [1, { id: 1, name: "Latte", price: 220 }],
  [2, { id: 2, name: "Masala Chai", price: 120 }],
]);

const group = new Hono();

// Create
group.post("/", async (c) => {
  const input = await c.req.json<MenuItemRequest>();
  const item: MenuItem = { id: Math.max(...menu.keys()) + 1, name: input.name, price: input.price };
  menu.set(item.id, item);
  c.header("Location", "/menu/" + item.id);
  return c.json(item, 201);
});

// Read: the list, and one item
group.get("/", (c) => c.json([...menu.values()], 200));
group.get("/:id", (c) => {
  const item = menu.get(Number(c.req.param("id")));
  return item ? c.json(item, 200) : c.body(null, 404);
});

// Update
group.put("/:id", async (c) => {
  const id = Number(c.req.param("id"));
  if (!menu.has(id)) return c.body(null, 404);
  const input = await c.req.json<MenuItemRequest>();
  menu.set(id, { id, name: input.name, price: input.price });
  return c.json(menu.get(id), 200);
});

// Delete
group.delete("/:id", (c) => (menu.delete(Number(c.req.param("id"))) ? c.body(null, 204) : c.body(null, 404)));

const app = new Hono();
app.route("/menu", group);

Content-Type: application/json

{"name":"Cold Coffee","price":180}

###
GET /menu

###
PUT /menu/3
Content-Type: application/json

{"name":"Cold Coffee","price":190}

###
DELETE /menu/2

###
GET /menu`;

var app = builder.Build();

var prices = new Dictionary<string, int> { ["Latte"] = 220, ["Masala Chai"] = 120, ["Blueberry Muffin"] = 150 };
var orders = new Dictionary<int, Order>();

// An order has several lines. The server works out every line total and the order total.
app.MapPost("/orders", (CreateOrderRequest input) =>
{
    var lines = input.Lines
        .Select(l => new OrderLine(l.Item, l.Quantity, prices[l.Item] * l.Quantity))
        .ToList();
    var order = new Order(orders.Count + 101, input.CustomerId, lines, lines.Sum(l => l.LineTotal), "received");
    orders[order.Id] = order;
    return Results.Created("/orders/" + order.Id, order);
});

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

app.Run();

record LineRequest(string Item, int Quantity);
record CreateOrderRequest(int CustomerId, List<LineRequest> Lines);
record OrderLine(string Item, int Quantity, int LineTotal);
record Order(int Id, int CustomerId, List<OrderLine> Lines, int Total, string Status);`;

type LineRequest = { item: string; quantity: number };
type CreateOrderRequest = { customerId: number; lines: LineRequest[] };
type OrderLine = { item: string; quantity: number; lineTotal: number };
type Order = { id: number; customerId: number; lines: OrderLine[]; total: number; status: string };

const prices = new Map<string, number>([["Latte", 220], ["Masala Chai", 120], ["Blueberry Muffin", 150]]);
const orders = new Map<number, Order>();
const app = new Hono();

// An order has several lines. The server works out every line total and the order total.
app.post("/orders", async (c) => {
  const input = await c.req.json<CreateOrderRequest>();
  const lines = input.lines.map((l) => ({ item: l.item, quantity: l.quantity, lineTotal: prices.get(l.item)! * l.quantity }));
  const order: Order = { id: orders.size + 101, customerId: input.customerId, lines, total: lines.reduce((s, l) => s + l.lineTotal, 0), status: "received" };
  orders.set(order.id, order);
  c.header("Location", "/orders/" + order.id);
  return c.json(order, 201);
});

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

Content-Type: application/json

{"customerId":7,"lines":[{"item":"Latte","quantity":2},{"item":"Blueberry Muffin","quantity":1}]}

###
GET /orders/101`;

var app = builder.Build();

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

// Which status may follow which. Anything else is a conflict.
var allowed = new Dictionary<string, string[]>
{
    ["received"] = new[] { "preparing", "cancelled" },
    ["preparing"] = new[] { "ready" },
    ["ready"] = new[] { "delivered" },
    ["delivered"] = Array.Empty<string>(),
    ["cancelled"] = Array.Empty<string>()
};

app.MapPatch("/orders/{id:int}", (int id, StatusChange change) =>
{
    if (!orders.TryGetValue(id, out var order)) return Results.NotFound();
    if (!allowed[order.Status].Contains(change.Status))
    {
        return Results.Problem(statusCode: 409, title: "Status change not allowed",
            detail: "An order that is " + order.Status + " cannot become " + change.Status + ".");
    }
    orders[id] = order with { Status = change.Status };
    return Results.Ok(orders[id]);
});

app.Run();

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

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

// Which status may follow which. Anything else is a conflict.
const allowed: Record<string, string[]> = {
  received: ["preparing", "cancelled"],
  preparing: ["ready"],
  ready: ["delivered"],
  delivered: [],
  cancelled: [],
};

function problem(c: Context, status: number, title: string, detail: string) {
  const body = { type: "https://tools.ietf.org/html/rfc9110#section-15.5.10", title, status, detail };
  return c.body(JSON.stringify(body), status, { "Content-Type": "application/problem+json" });
}

const app = new Hono();

app.patch("/orders/:id", async (c) => {
  const id = Number(c.req.param("id"));
  const order = orders.get(id);
  if (!order) return c.body(null, 404);
  const change = await c.req.json<{ status: string }>();
  if (!allowed[order.status].includes(change.status)) {
    return problem(c, 409, "Status change not allowed", "An order that is " + order.status + " cannot become " + change.status + ".");
  }
  const updated = { ...order, status: change.status };
  orders.set(id, updated);
  return c.json(updated, 200);
});

Content-Type: application/json

{"status":"preparing"}

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

{"status":"delivered"}

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

{"status":"ready"}`;

// The service holds the data and the rules. It is registered once.
builder.Services.AddSingleton<MenuService>();

var app = builder.Build();

// The endpoints live in their own class. Program.cs stays short.
app.MapMenuEndpoints();

app.Run();

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

// Service: the kitchen. No HTTP in here, just data and rules.
class MenuService
{
    private readonly Dictionary<int, MenuItem> _items = new()
    {
        [1] = new(1, "Latte", 220),
        [2] = new(2, "Masala Chai", 120)
    };

    public IEnumerable<MenuItem> All() => _items.Values;
    public MenuItem? Find(int id) => _items.GetValueOrDefault(id);
}

// Endpoints: the waiter. Only HTTP in here: routes, status codes, replies.
static class MenuEndpoints
{
    public static void MapMenuEndpoints(this WebApplication app)
    {
        var menu = app.MapGroup("/menu");
        menu.MapGet("/", (MenuService service) => Results.Ok(service.All()));
        menu.MapGet("/{id:int}", (int id, MenuService service) =>
            service.Find(id) is { } item ? Results.Ok(item) : Results.NotFound());
    }
}`;

type MenuItem = { id: number; name: string; price: number };

// Service: the kitchen. No HTTP in here, just data and rules.
class MenuService {
  private items = new Map<number, MenuItem>([
    [1, { id: 1, name: "Latte", price: 220 }],
    [2, { id: 2, name: "Masala Chai", price: 120 }],
  ]);

  all(): MenuItem[] {
    return [...this.items.values()];
  }

  find(id: number): MenuItem | undefined {
    return this.items.get(id);
  }
}

// Routes: the waiter. Only HTTP in here: routes, status codes, replies.
function menuRoutes(service: MenuService) {
  const routes = new Hono();
  routes.get("/", (c) => c.json(service.all(), 200));
  routes.get("/:id", (c) => {
    const item = service.find(Number(c.req.param("id")));
    return item ? c.json(item, 200) : c.body(null, 404);
  });
  return routes;
}

// One service, created once, handed to the routes.
const service = new MenuService();
const app = new Hono();
app.route("/menu", menuRoutes(service));

###
GET /menu/2`;

var app = builder.Build();

var menu = new Dictionary<int, MenuItem> { [1] = new(1, "Latte", 220), [2] = new(2, "Masala Chai", 120) };
var orders = new Dictionary<int, Order>();

app.MapGet("/menu", () => Results.Ok(menu.Values));

app.MapPost("/orders", (CreateOrderRequest input) =>
{
    if (!menu.TryGetValue(input.MenuItemId, out var item))
        return Results.ValidationProblem(new Dictionary<string, string[]> { ["menuItemId"] = new[] { "No such menu item." } });
    if (input.Quantity < 1 || input.Quantity > 10)
        return Results.ValidationProblem(new Dictionary<string, string[]> { ["quantity"] = new[] { "Quantity must be between 1 and 10." } });
    var order = new Order(orders.Count + 101, item.Name, input.Quantity, item.Price * input.Quantity, "received");
    orders[order.Id] = order;
    return Results.Created("/orders/" + order.Id, order);
});

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

app.MapDelete("/orders/{id:int}", (int id) =>
{
    if (!orders.TryGetValue(id, out var order)) return Results.NotFound();
    if (order.Status != "received")
        return Results.Problem(statusCode: 409, title: "Too late to cancel", detail: "The kitchen has started this order.");
    orders.Remove(id);
    return Results.NoContent();
});

app.Run();

record MenuItem(int Id, string Name, int Price);
record CreateOrderRequest(int MenuItemId, int Quantity);
record Order(int Id, string Item, int Quantity, int Total, string Status);`;

type MenuItem = { id: number; name: string; price: number };
type CreateOrderRequest = { menuItemId: number; quantity: number };
type Order = { id: number; item: string; quantity: number; total: number; status: string };

const menu = new Map<number, MenuItem>([[1, { id: 1, name: "Latte", price: 220 }], [2, { id: 2, name: "Masala Chai", price: 120 }]]);
const orders = new Map<number, Order>();

const TYPES: Record<number, string> = { 400: "https://tools.ietf.org/html/rfc9110#section-15.5.1", 409: "https://tools.ietf.org/html/rfc9110#section-15.5.10" };
function problem(c: Context, status: number, title: string, extra: Record<string, unknown> = {}) {
  return c.body(JSON.stringify({ type: TYPES[status], title, status, ...extra }), status, { "Content-Type": "application/problem+json" });
}
const invalid = (c: Context, field: string, message: string) =>
  problem(c, 400, "One or more validation errors occurred.", { errors: { [field]: [message] } });

const app = new Hono();

app.get("/menu", (c) => c.json([...menu.values()], 200));

app.post("/orders", async (c) => {
  const input = await c.req.json<CreateOrderRequest>();
  const item = menu.get(input.menuItemId);
  if (!item) return invalid(c, "menuItemId", "No such menu item.");
  if (input.quantity < 1 || input.quantity > 10) return invalid(c, "quantity", "Quantity must be between 1 and 10.");
  const order: Order = { id: orders.size + 101, item: item.name, quantity: input.quantity, total: item.price * input.quantity, status: "received" };
  orders.set(order.id, order);
  c.header("Location", "/orders/" + order.id);
  return c.json(order, 201);
});

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

app.delete("/orders/:id", (c) => {
  const id = Number(c.req.param("id"));
  const order = orders.get(id);
  if (!order) return c.body(null, 404);
  if (order.status !== "received") return problem(c, 409, "Too late to cancel", { detail: "The kitchen has started this order." });
  orders.delete(id);
  return c.body(null, 204);
});

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

{"menuItemId":1,"quantity":2}

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

{"menuItemId":9,"quantity":1}

###
GET /orders/101

###
DELETE /orders/101

###
GET /orders/101`;

  {
    prompt: "Add DELETE /menu/{id}. Return 204 when the item is removed, and 404 when it does not exist.",
    hint: "Look the item up. If it is missing, return 404. Otherwise, remove it and return 204.",
    explain: "Delete answers 204 the first time and 404 after that, and the item is gone either way.",
    csharp: { starter: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var menu = new Dictionary<int, MenuItem>
{
    [1] = new(1, "Latte", 220),
    [2] = new(2, "Masala Chai", 120)
};

var group = app.MapGroup("/menu");

// Create
group.MapPost("/", (MenuItemRequest input) =>
{
    var item = new MenuItem(menu.Keys.Max() + 1, input.Name, input.Price);
    menu[item.Id] = item;
    return Results.Created("/menu/" + item.Id, item);
});

// Read: the list, and one item
group.MapGet("/", () => Results.Ok(menu.Values));
group.MapGet("/{id:int}", (int id) =>
    menu.TryGetValue(id, out var item) ? Results.Ok(item) : Results.NotFound());

// Update
group.MapPut("/{id:int}", (int id, MenuItemRequest input) =>
{
    if (!menu.ContainsKey(id)) return Results.NotFound();
    menu[id] = new MenuItem(id, input.Name, input.Price);
    return Results.Ok(menu[id]);
});

// Delete: add DELETE /menu/{id}. 204 when removed, 404 when it does not exist.

app.Run();

record MenuItemRequest(string Name, int Price);
record MenuItem(int Id, string Name, int Price);`, solution: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var menu = new Dictionary<int, MenuItem>
{
    [1] = new(1, "Latte", 220),
    [2] = new(2, "Masala Chai", 120)
};

var group = app.MapGroup("/menu");

// Create
group.MapPost("/", (MenuItemRequest input) =>
{
    var item = new MenuItem(menu.Keys.Max() + 1, input.Name, input.Price);
    menu[item.Id] = item;
    return Results.Created("/menu/" + item.Id, item);
});

// Read: the list, and one item
group.MapGet("/", () => Results.Ok(menu.Values));
group.MapGet("/{id:int}", (int id) =>
    menu.TryGetValue(id, out var item) ? Results.Ok(item) : Results.NotFound());

// Update
group.MapPut("/{id:int}", (int id, MenuItemRequest input) =>
{
    if (!menu.ContainsKey(id)) return Results.NotFound();
    menu[id] = new MenuItem(id, input.Name, input.Price);
    return Results.Ok(menu[id]);
});

// Delete: add DELETE /menu/{id}. 204 when removed, 404 when it does not exist.
group.MapDelete("/{id:int}", (int id) =>
    menu.Remove(id) ? Results.NoContent() : Results.NotFound());

app.Run();

record MenuItemRequest(string Name, int Price);
record MenuItem(int Id, string Name, int Price);`, mode: "aspnet", requests: `DELETE /menu/2

###
DELETE /menu/2

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

type MenuItemRequest = { name: string; price: number };
type MenuItem = { id: number; name: string; price: number };

const menu = new Map<number, MenuItem>([
  [1, { id: 1, name: "Latte", price: 220 }],
  [2, { id: 2, name: "Masala Chai", price: 120 }],
]);

const group = new Hono();

// Create
group.post("/", async (c) => {
  const input = await c.req.json<MenuItemRequest>();
  const item: MenuItem = { id: Math.max(...menu.keys()) + 1, name: input.name, price: input.price };
  menu.set(item.id, item);
  c.header("Location", "/menu/" + item.id);
  return c.json(item, 201);
});

// Read: the list, and one item
group.get("/", (c) => c.json([...menu.values()], 200));
group.get("/:id", (c) => {
  const item = menu.get(Number(c.req.param("id")));
  return item ? c.json(item, 200) : c.body(null, 404);
});

// Update
group.put("/:id", async (c) => {
  const id = Number(c.req.param("id"));
  if (!menu.has(id)) return c.body(null, 404);
  const input = await c.req.json<MenuItemRequest>();
  menu.set(id, { id, name: input.name, price: input.price });
  return c.json(menu.get(id), 200);
});

// Delete: add DELETE /menu/{id}. 204 when removed, 404 when it does not exist.

const app = new Hono();
app.route("/menu", group);

type MenuItemRequest = { name: string; price: number };
type MenuItem = { id: number; name: string; price: number };

const menu = new Map<number, MenuItem>([
  [1, { id: 1, name: "Latte", price: 220 }],
  [2, { id: 2, name: "Masala Chai", price: 120 }],
]);

const group = new Hono();

// Create
group.post("/", async (c) => {
  const input = await c.req.json<MenuItemRequest>();
  const item: MenuItem = { id: Math.max(...menu.keys()) + 1, name: input.name, price: input.price };
  menu.set(item.id, item);
  c.header("Location", "/menu/" + item.id);
  return c.json(item, 201);
});

// Read: the list, and one item
group.get("/", (c) => c.json([...menu.values()], 200));
group.get("/:id", (c) => {
  const item = menu.get(Number(c.req.param("id")));
  return item ? c.json(item, 200) : c.body(null, 404);
});

// Update
group.put("/:id", async (c) => {
  const id = Number(c.req.param("id"));
  if (!menu.has(id)) return c.body(null, 404);
  const input = await c.req.json<MenuItemRequest>();
  menu.set(id, { id, name: input.name, price: input.price });
  return c.json(menu.get(id), 200);
});

// Delete: add DELETE /menu/{id}. 204 when removed, 404 when it does not exist.
group.delete("/:id", (c) => (menu.delete(Number(c.req.param("id"))) ? c.body(null, 204) : c.body(null, 404)));

const app = new Hono();
app.route("/menu", group);

###
DELETE /menu/2

###
GET /menu` },
    checks: `DELETE /menu/99

###
DELETE /menu/1

###
GET /menu`
  },
  {
    prompt: "PUT /menu/42 should not crash or create an item. Return 404 when the id does not exist.",
    hint: "Check whether the id is in the menu before you replace it.",
    explain: "Update works only on things that exist. A missing id is 404.",
    csharp: { starter: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var menu = new Dictionary<int, MenuItem>
{
    [1] = new(1, "Latte", 220),
    [2] = new(2, "Masala Chai", 120)
};

var group = app.MapGroup("/menu");

// Create
group.MapPost("/", (MenuItemRequest input) =>
{
    var item = new MenuItem(menu.Keys.Max() + 1, input.Name, input.Price);
    menu[item.Id] = item;
    return Results.Created("/menu/" + item.Id, item);
});

// Read: the list, and one item
group.MapGet("/", () => Results.Ok(menu.Values));
group.MapGet("/{id:int}", (int id) =>
    menu.TryGetValue(id, out var item) ? Results.Ok(item) : Results.NotFound());

// Update: PUT on an id that does not exist must answer 404, not create it.
group.MapPut("/{id:int}", (int id, MenuItemRequest input) =>
{
    menu[id] = new MenuItem(id, input.Name, input.Price);
    return Results.Ok(menu[id]);
});

// Delete
group.MapDelete("/{id:int}", (int id) =>
    menu.Remove(id) ? Results.NoContent() : Results.NotFound());

app.Run();

record MenuItemRequest(string Name, int Price);
record MenuItem(int Id, string Name, int Price);`, solution: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var menu = new Dictionary<int, MenuItem>
{
    [1] = new(1, "Latte", 220),
    [2] = new(2, "Masala Chai", 120)
};

var group = app.MapGroup("/menu");

// Create
group.MapPost("/", (MenuItemRequest input) =>
{
    var item = new MenuItem(menu.Keys.Max() + 1, input.Name, input.Price);
    menu[item.Id] = item;
    return Results.Created("/menu/" + item.Id, item);
});

// Read: the list, and one item
group.MapGet("/", () => Results.Ok(menu.Values));
group.MapGet("/{id:int}", (int id) =>
    menu.TryGetValue(id, out var item) ? Results.Ok(item) : Results.NotFound());

// Update: PUT on an id that does not exist must answer 404, not create it.
group.MapPut("/{id:int}", (int id, MenuItemRequest input) =>
{
    if (!menu.ContainsKey(id)) return Results.NotFound();
    menu[id] = new MenuItem(id, input.Name, input.Price);
    return Results.Ok(menu[id]);
});

// Delete
group.MapDelete("/{id:int}", (int id) =>
    menu.Remove(id) ? Results.NoContent() : Results.NotFound());

app.Run();

record MenuItemRequest(string Name, int Price);
record MenuItem(int Id, string Name, int Price);`, mode: "aspnet", requests: `PUT /menu/42
Content-Type: application/json

{"name":"Ghost Latte","price":1}

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

type MenuItemRequest = { name: string; price: number };
type MenuItem = { id: number; name: string; price: number };

const menu = new Map<number, MenuItem>([
  [1, { id: 1, name: "Latte", price: 220 }],
  [2, { id: 2, name: "Masala Chai", price: 120 }],
]);

const group = new Hono();

// Create
group.post("/", async (c) => {
  const input = await c.req.json<MenuItemRequest>();
  const item: MenuItem = { id: Math.max(...menu.keys()) + 1, name: input.name, price: input.price };
  menu.set(item.id, item);
  c.header("Location", "/menu/" + item.id);
  return c.json(item, 201);
});

// Read: the list, and one item
group.get("/", (c) => c.json([...menu.values()], 200));
group.get("/:id", (c) => {
  const item = menu.get(Number(c.req.param("id")));
  return item ? c.json(item, 200) : c.body(null, 404);
});

// Update: PUT on an id that does not exist must answer 404, not create it.
group.put("/:id", async (c) => {
  const id = Number(c.req.param("id"));
  const input = await c.req.json<MenuItemRequest>();
  menu.set(id, { id, name: input.name, price: input.price });
  return c.json(menu.get(id), 200);
});

// Delete
group.delete("/:id", (c) => (menu.delete(Number(c.req.param("id"))) ? c.body(null, 204) : c.body(null, 404)));

const app = new Hono();
app.route("/menu", group);

type MenuItemRequest = { name: string; price: number };
type MenuItem = { id: number; name: string; price: number };

const menu = new Map<number, MenuItem>([
  [1, { id: 1, name: "Latte", price: 220 }],
  [2, { id: 2, name: "Masala Chai", price: 120 }],
]);

const group = new Hono();

// Create
group.post("/", async (c) => {
  const input = await c.req.json<MenuItemRequest>();
  const item: MenuItem = { id: Math.max(...menu.keys()) + 1, name: input.name, price: input.price };
  menu.set(item.id, item);
  c.header("Location", "/menu/" + item.id);
  return c.json(item, 201);
});

// Read: the list, and one item
group.get("/", (c) => c.json([...menu.values()], 200));
group.get("/:id", (c) => {
  const item = menu.get(Number(c.req.param("id")));
  return item ? c.json(item, 200) : c.body(null, 404);
});

// Update: PUT on an id that does not exist must answer 404, not create it.
group.put("/:id", async (c) => {
  const id = Number(c.req.param("id"));
  if (!menu.has(id)) return c.body(null, 404);
  const input = await c.req.json<MenuItemRequest>();
  menu.set(id, { id, name: input.name, price: input.price });
  return c.json(menu.get(id), 200);
});

// Delete
group.delete("/:id", (c) => (menu.delete(Number(c.req.param("id"))) ? c.body(null, 204) : c.body(null, 404)));

const app = new Hono();
app.route("/menu", group);

Content-Type: application/json

{"name":"Ghost Latte","price":1}

###
GET /menu` },
    checks: `PUT /menu/7
Content-Type: application/json

{"name":"Cold Coffee","price":180}

###
PUT /menu/1
Content-Type: application/json

{"name":"Latte","price":250}

###
GET /menu`
  },
  {
    prompt: "Work out each line total and the order total on the server, from the menu prices.",
    hint: "For each line, multiply the price by the quantity. Then add up the line totals.",
    explain: "Every number in the reply comes from the server, so a client cannot change it.",
    csharp: { starter: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var prices = new Dictionary<string, int> { ["Latte"] = 220, ["Masala Chai"] = 120, ["Blueberry Muffin"] = 150 };
var orders = new Dictionary<int, Order>();

// The order total is always 0. Make it the sum of all line totals.
app.MapPost("/orders", (CreateOrderRequest input) =>
{
    var lines = input.Lines
        .Select(l => new OrderLine(l.Item, l.Quantity, prices[l.Item] * l.Quantity))
        .ToList();
    var order = new Order(orders.Count + 101, input.CustomerId, lines, 0, "received");
    orders[order.Id] = order;
    return Results.Created("/orders/" + order.Id, order);
});

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

app.Run();

record LineRequest(string Item, int Quantity);
record CreateOrderRequest(int CustomerId, List<LineRequest> Lines);
record OrderLine(string Item, int Quantity, int LineTotal);
record Order(int Id, int CustomerId, List<OrderLine> Lines, int Total, string Status);`, solution: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var prices = new Dictionary<string, int> { ["Latte"] = 220, ["Masala Chai"] = 120, ["Blueberry Muffin"] = 150 };
var orders = new Dictionary<int, Order>();

// The order total is always 0. Make it the sum of all line totals.
app.MapPost("/orders", (CreateOrderRequest input) =>
{
    var lines = input.Lines
        .Select(l => new OrderLine(l.Item, l.Quantity, prices[l.Item] * l.Quantity))
        .ToList();
    var order = new Order(orders.Count + 101, input.CustomerId, lines, lines.Sum(l => l.LineTotal), "received");
    orders[order.Id] = order;
    return Results.Created("/orders/" + order.Id, order);
});

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

app.Run();

record LineRequest(string Item, int Quantity);
record CreateOrderRequest(int CustomerId, List<LineRequest> Lines);
record OrderLine(string Item, int Quantity, int LineTotal);
record Order(int Id, int CustomerId, List<OrderLine> Lines, int Total, string Status);`, mode: "aspnet", requests: `POST /orders
Content-Type: application/json

{"customerId":7,"lines":[{"item":"Latte","quantity":2},{"item":"Blueberry Muffin","quantity":1}]}

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

type LineRequest = { item: string; quantity: number };
type CreateOrderRequest = { customerId: number; lines: LineRequest[] };
type OrderLine = { item: string; quantity: number; lineTotal: number };
type Order = { id: number; customerId: number; lines: OrderLine[]; total: number; status: string };

const prices = new Map<string, number>([["Latte", 220], ["Masala Chai", 120], ["Blueberry Muffin", 150]]);
const orders = new Map<number, Order>();
const app = new Hono();

// The order total is always 0. Make it the sum of all line totals.
app.post("/orders", async (c) => {
  const input = await c.req.json<CreateOrderRequest>();
  const lines = input.lines.map((l) => ({ item: l.item, quantity: l.quantity, lineTotal: prices.get(l.item)! * l.quantity }));
  const order: Order = { id: orders.size + 101, customerId: input.customerId, lines, total: 0, status: "received" };
  orders.set(order.id, order);
  c.header("Location", "/orders/" + order.id);
  return c.json(order, 201);
});

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

type LineRequest = { item: string; quantity: number };
type CreateOrderRequest = { customerId: number; lines: LineRequest[] };
type OrderLine = { item: string; quantity: number; lineTotal: number };
type Order = { id: number; customerId: number; lines: OrderLine[]; total: number; status: string };

const prices = new Map<string, number>([["Latte", 220], ["Masala Chai", 120], ["Blueberry Muffin", 150]]);
const orders = new Map<number, Order>();
const app = new Hono();

// The order total is always 0. Make it the sum of all line totals.
app.post("/orders", async (c) => {
  const input = await c.req.json<CreateOrderRequest>();
  const lines = input.lines.map((l) => ({ item: l.item, quantity: l.quantity, lineTotal: prices.get(l.item)! * l.quantity }));
  const order: Order = { id: orders.size + 101, customerId: input.customerId, lines, total: lines.reduce((s, l) => s + l.lineTotal, 0), status: "received" };
  orders.set(order.id, order);
  c.header("Location", "/orders/" + order.id);
  return c.json(order, 201);
});

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

Content-Type: application/json

{"customerId":7,"lines":[{"item":"Latte","quantity":2},{"item":"Blueberry Muffin","quantity":1}]}

###
GET /orders/101` },
    checks: `POST /orders
Content-Type: application/json

{"customerId":8,"lines":[{"item":"Masala Chai","quantity":3},{"item":"Latte","quantity":1}]}

###
GET /orders/102

###
GET /orders/999`
  }
];
