# REST API Design and URL Naming

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

> Turn action-style endpoints into clean REST resource URLs, nest related resources, and add filtering, sorting and capped pagination with the query string.

A **REST API** organizes data as **resources**. A resource is any thing that the API manages, such as an order, customer, menu item, or product. In REST, the URL identifies **which resource** you want to work with, while the HTTP method tells the server **what action** to perform on that resource. This keeps URLs simple, predictable, and easy to understand.

For example:

- `GET /orders` reads orders.
- `POST /orders` creates a new order.
- `GET /orders/101` reads a specific order.
- `DELETE /orders/101` removes a specific order.

A good REST API uses clear resource names instead of action names. This makes it easier for developers to learn the API, predict URLs, and build applications without constantly checking documentation.

## Watch the Endpoints Multiply

The Brew & Bytes API started with only a few endpoints. Then the application needed to show:

- All orders for a customer.
- Open orders for a customer.
- Open orders sorted by total.

Each new requirement created another endpoint named after an action.

It works, but the URL names become longer and harder to manage:

- `/getOrdersByCustomerId`
- `/getOpenOrdersByCustomerId`
- `/getOpenOrdersByCustomerIdSortedByTotal`

As more features are added, more action-based URLs appear. Soon there are dozens of endpoints, and it becomes difficult to remember what exists or predict what the next URL should be.

**In simple words:** action-based URLs become messy as an API grows. REST solves this problem by using a small number of resource-based URLs that can answer many different requests.

## TL;DR

- A **resource** is a thing managed by the API, such as an order, customer, or menu item.
- Use **plural nouns** for collections: `/orders`, `/customers`.
- Use an id to access a single resource: `/orders/101`.
- Put related resources under their parent resource: `/customers/7/orders`.
- Let the HTTP method describe the action.
- Do not put words such as `get`, `create`, or `delete` in URLs.
- Use query strings for filtering, sorting, and pagination.
- Always limit the maximum page size.

## A Real-World Example

Think about a library. You do not ask for a book by describing an action such as: Get the book from shelf 4.

Instead, you identify where the book is located.

For example: Fiction → Shelf 4 → Book 12

The location identifies the item. What you do with it, such as borrowing or returning it, is a separate action. REST follows the same idea. A URL identifies a resource: /customers/7/orders 
The HTTP method decides what happens:

- `GET` reads the orders.
- `POST` creates a new order.

## What You Will Learn

In this lesson, you will learn:

1. What a resource is.
2. How to create REST-style URLs.
3. How to use filtering and sorting.
4. How pagination works.
5. How URL rules can be checked automatically.

## Nouns, Not Verbs

The same API can be designed using resource names instead of action names.

| Old URL | REST URL |
|---|---|
| `GET /getAllOrders` | `GET /orders` |
| `GET /getOrderById?id=101` | `GET /orders/101` |
| `GET /getOrdersByCustomerId?id=7` | `GET /customers/7/orders` |
| `POST /createOrder` | `POST /orders` |
| `POST /deleteOrderById?id=101` | `DELETE /orders/101` |

If order `999` does not exist: GET /orders/999

the server should return: 404 Not Found because the requested resource does not exist. In C#, `{id:int}` means that the route expects a numeric id. In Hono, `:id` works the same way and can be accessed with: c.req.param("id")

REST URLs should be simple, consistent, and easy to read. Use plural nouns (such as /orders), keep URLs in lowercase, use hyphens between words (/menu-items), avoid verbs, and place resource IDs in the path. Also, avoid deep nesting whenever possible. For example, /customers/7/orders is clear and readable, while /customers/7/orders/101/items/3/notes is unnecessarily complex.

## Filter and Sort

Instead of creating a new endpoint for every search requirement, use query strings.

For example:

```text
/orders?status=open&sort=-total
```

This request means:

- Show only open orders.
- Sort by total value.
- Highest totals first.

The minus sign (`-`) commonly means descending order.

Examples:

```text
sort=total
```

Smallest to largest.

```text
sort=-total
```

Largest to smallest.

Using filters and sorting allows one endpoint to support many different requests.

## Pagination

A small shop might have a few orders.

A real system could have thousands or millions.

Returning every record in one response would be slow and inefficient.

Pagination solves this problem by returning a small page of results at a time.

Example: ?page=2&pageSize=2

The response includes:

- Current page
- Page size
- Total number of records
- Data for that page

The client can use the total count to know:

- How many records exist
- Whether more pages are available

### Why Limit Page Size?

A client may try: ?pageSize=500 or even: ?pageSize=1000000

Returning that much data can overload the API. For this reason, APIs often enforce a maximum size. Example: Maximum page size = 100 Even if the client requests more, the server returns only 100 items.

**Always cap the page size.** Without a limit, any client can request every row in one call. This is a real security risk: it is one of the OWASP API Security Top 10, called *unrestricted resource consumption*.

### Page Numbers vs Cursors

This lesson uses page numbers because they are easy to understand.

Example: ?page=2&pageSize=20

However, when new records are added frequently, page numbers can cause duplicate or missed results.

Many large APIs use **cursor pagination** instead.

A cursor says: Give me the next 20 records after this item.

This keeps the results stable even while new records are being created.

## Check URLs Automatically

Teams often create API design rules.

A URL linter can automatically check those rules.

Examples:

Bad:/getOrdersByCustomerId
Bad: /menu_items
Good: /orders
Good: /customers/7/orders

Automatic checks help teams keep URL naming consistent.

Write your URL rules down once and share them with the team. A short style guide ("plural nouns, lowercase, hyphens, ids in the path, filters in the query string") stops a new endpoint from breaking the pattern.

## Cursor Pagination

Cursor pagination avoids problems that happen when data changes between requests. Instead of requesting a page number, the client sends a cursor.

Example:

First response: "nextCursor": "aWQ6MTA1"

The client sends that cursor back: ?after=aWQ6MTA1

The server then returns the next set of results. Even if a new order appears meanwhile, records are not duplicated or skipped. When no more data exists, the API returns: "nextCursor": null
This tells the client that pagination has reached the end.

Treat the cursor as an opaque string. Clients should send it back exactly as they got it and never build one themselves. Then you can change what is inside it later without breaking anyone.

## Where You See This

- Public APIs use resource-based URLs such as `/customers/{id}` and `/orders/{id}`.
- Admin dashboards use filtering, sorting, and pagination on data tables.
- Mobile applications load large lists one page at a time.
- E-commerce applications organize products, customers, and orders as resources.

## Common mistakes

| Symptom | Cause | Fix |
|---|---|---|
| Dozens of near-identical endpoints | One endpoint for each question, named after an action | One resource URL, with filters in the query string |
| Clients cannot guess URLs | Mixed styles: `/getMenu`, `/order_list`, `/Customers` | Use plural nouns, lowercase and hyphens everywhere |
| A list request is slow and huge | There is no page size limit | Add pages, and cap the size |
| The app shows an item twice while scrolling | Page numbers are used on a list that changes fast | Use a cursor ("after id 105") for that list |
| Apps break when you change the cursor format | Clients build or decode cursors themselves | Treat `nextCursor` as opaque, and send it back unchanged |

## Exercises

## What to Remember

- A URL names a **resource**. The **method** says what to do with it.
- Use plural nouns, lowercase, hyphens and no verbs: `/orders`, `/orders/101`, `/customers/7/orders`.
- Filter, sort and page with the **query string**.
- Always **cap** the page size.
- Return `404` when the URL names something that does not exist.

**Next:** HTTP Status Codes and Error Handling. You will make every reply say clearly whether it worked, and why not.

var app = builder.Build();

var orders = new List<Order>
{
    new(101, 7, "Latte", 220, "delivered"),
    new(102, 8, "Masala Chai", 240, "open"),
    new(103, 7, "Cold Coffee", 360, "open"),
    new(104, 9, "Blueberry Muffin", 150, "delivered"),
    new(105, 7, "Latte", 440, "open"),
    new(106, 8, "Latte", 220, "cancelled")
};

// Each URL is named after an action. Every new question needs a new endpoint.
app.MapGet("/getAllOrders", () => TypedResults.Ok(orders));
app.MapGet("/getOrderById", (int id) => TypedResults.Ok(orders.Where(o => o.Id == id)));
app.MapGet("/getOrdersByCustomerId", (int id) => TypedResults.Ok(orders.Where(o => o.CustomerId == id)));
app.MapGet("/getOpenOrdersByCustomerId", (int id) => TypedResults.Ok(orders.Where(o => o.CustomerId == id && o.Status == "open")));

app.Run();

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

type Order = { id: number; customerId: number; item: string; total: number; status: string };

const orders: Order[] = [
  { id: 101, customerId: 7, item: "Latte", total: 220, status: "delivered" },
  { id: 102, customerId: 8, item: "Masala Chai", total: 240, status: "open" },
  { id: 103, customerId: 7, item: "Cold Coffee", total: 360, status: "open" },
  { id: 104, customerId: 9, item: "Blueberry Muffin", total: 150, status: "delivered" },
  { id: 105, customerId: 7, item: "Latte", total: 440, status: "open" },
  { id: 106, customerId: 8, item: "Latte", total: 220, status: "cancelled" },
];

const app = new Hono();

// Each URL is named after an action. Every new question needs a new route.
app.get("/getAllOrders", (c) => c.json(orders, 200));
app.get("/getOrderById", (c) => c.json(orders.filter((o) => o.id === Number(c.req.query("id"))), 200));
app.get("/getOrdersByCustomerId", (c) => c.json(orders.filter((o) => o.customerId === Number(c.req.query("id"))), 200));
app.get("/getOpenOrdersByCustomerId", (c) =>
  c.json(orders.filter((o) => o.customerId === Number(c.req.query("id")) && o.status === "open"), 200));

###
GET /getOpenOrdersByCustomerId?id=7`;

var app = builder.Build();

var orders = new List<Order>
{
    new(101, 7, "Latte", 220, "delivered"),
    new(102, 8, "Masala Chai", 240, "open"),
    new(103, 7, "Cold Coffee", 360, "open"),
    new(104, 9, "Blueberry Muffin", 150, "delivered"),
    new(105, 7, "Latte", 440, "open"),
    new(106, 8, "Latte", 220, "cancelled")
};

// Nouns, not verbs. One collection, one item, and the orders that belong to a customer.
app.MapGet("/orders", () => TypedResults.Ok(orders));

app.MapGet("/orders/{id:int}", (int id) =>
    orders.FirstOrDefault(o => o.Id == id) is { } order ? Results.Ok(order) : Results.NotFound());

app.MapGet("/customers/{customerId:int}/orders", (int customerId) =>
    TypedResults.Ok(orders.Where(o => o.CustomerId == customerId)));

app.Run();

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

type Order = { id: number; customerId: number; item: string; total: number; status: string };

const orders: Order[] = [
  { id: 101, customerId: 7, item: "Latte", total: 220, status: "delivered" },
  { id: 102, customerId: 8, item: "Masala Chai", total: 240, status: "open" },
  { id: 103, customerId: 7, item: "Cold Coffee", total: 360, status: "open" },
  { id: 104, customerId: 9, item: "Blueberry Muffin", total: 150, status: "delivered" },
  { id: 105, customerId: 7, item: "Latte", total: 440, status: "open" },
  { id: 106, customerId: 8, item: "Latte", total: 220, status: "cancelled" },
];

const app = new Hono();

// Nouns, not verbs. One collection, one item, and the orders that belong to a customer.
app.get("/orders", (c) => c.json(orders, 200));

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

app.get("/customers/:customerId/orders", (c) =>
  c.json(orders.filter((o) => o.customerId === Number(c.req.param("customerId"))), 200));

###
GET /orders/104

###
GET /orders/999`;

var app = builder.Build();

var orders = new List<Order>
{
    new(101, 7, "Latte", 220, "delivered"),
    new(102, 8, "Masala Chai", 240, "open"),
    new(103, 7, "Cold Coffee", 360, "open"),
    new(104, 9, "Blueberry Muffin", 150, "delivered"),
    new(105, 7, "Latte", 440, "open"),
    new(106, 8, "Latte", 220, "cancelled")
};

// One endpoint answers many questions through the query string.
// ?status=open filters. ?sort=total sorts low to high, ?sort=-total high to low.
app.MapGet("/orders", (string? status, string? sort) =>
{
    IEnumerable<Order> result = orders;
    if (status is not null) result = result.Where(o => o.Status == status);
    if (sort == "total") result = result.OrderBy(o => o.Total);
    if (sort == "-total") result = result.OrderByDescending(o => o.Total);
    return TypedResults.Ok(result);
});

app.Run();

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

type Order = { id: number; customerId: number; item: string; total: number; status: string };

const orders: Order[] = [
  { id: 101, customerId: 7, item: "Latte", total: 220, status: "delivered" },
  { id: 102, customerId: 8, item: "Masala Chai", total: 240, status: "open" },
  { id: 103, customerId: 7, item: "Cold Coffee", total: 360, status: "open" },
  { id: 104, customerId: 9, item: "Blueberry Muffin", total: 150, status: "delivered" },
  { id: 105, customerId: 7, item: "Latte", total: 440, status: "open" },
  { id: 106, customerId: 8, item: "Latte", total: 220, status: "cancelled" },
];

const app = new Hono();

// One route answers many questions through the query string.
// ?status=open filters. ?sort=total sorts low to high, ?sort=-total high to low.
app.get("/orders", (c) => {
  const status = c.req.query("status");
  const sort = c.req.query("sort");
  let result = [...orders];
  if (status !== undefined) result = result.filter((o) => o.status === status);
  if (sort === "total") result.sort((a, b) => a.total - b.total);
  if (sort === "-total") result.sort((a, b) => b.total - a.total);
  return c.json(result, 200);
});

var app = builder.Build();

var orders = new List<Order>
{
    new(101, 7, "Latte", 220, "delivered"),
    new(102, 8, "Masala Chai", 240, "open"),
    new(103, 7, "Cold Coffee", 360, "open"),
    new(104, 9, "Blueberry Muffin", 150, "delivered"),
    new(105, 7, "Latte", 440, "open"),
    new(106, 8, "Latte", 220, "cancelled")
};

// Send the list one page at a time. The client asks for a page and a size.
// The server caps the size, so nobody can ask for a million rows at once.
app.MapGet("/orders", (int page = 1, int pageSize = 2) =>
{
    pageSize = Math.Clamp(pageSize, 1, 3);
    var items = orders.Skip((page - 1) * pageSize).Take(pageSize).ToList();
    return TypedResults.Ok(new { items, page, pageSize, total = orders.Count });
});

app.Run();

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

type Order = { id: number; customerId: number; item: string; total: number; status: string };

const orders: Order[] = [
  { id: 101, customerId: 7, item: "Latte", total: 220, status: "delivered" },
  { id: 102, customerId: 8, item: "Masala Chai", total: 240, status: "open" },
  { id: 103, customerId: 7, item: "Cold Coffee", total: 360, status: "open" },
  { id: 104, customerId: 9, item: "Blueberry Muffin", total: 150, status: "delivered" },
  { id: 105, customerId: 7, item: "Latte", total: 440, status: "open" },
  { id: 106, customerId: 8, item: "Latte", total: 220, status: "cancelled" },
];

const app = new Hono();

// Send the list one page at a time. The client asks for a page and a size.
// The server caps the size, so nobody can ask for a million rows at once.
app.get("/orders", (c) => {
  const page = Number(c.req.query("page") ?? 1);
  const pageSize = Math.min(Math.max(Number(c.req.query("pageSize") ?? 2), 1), 3);
  const items = orders.slice((page - 1) * pageSize, page * pageSize);
  return c.json({ items, page, pageSize, total: orders.length }, 200);
});

###
GET /orders?pageSize=500`;

string[] verbs = { "get", "create", "update", "delete", "add", "remove", "fetch" };

List<string> Check(string url)
{
    var problems = new List<string>();
    string path = url.Split('?')[0];
    foreach (string part in path.Trim('/').Split('/'))
    {
        if (part.StartsWith("{")) continue;
        if (verbs.Any(v => part.StartsWith(v, StringComparison.OrdinalIgnoreCase))) problems.Add("verb in \\"" + part + "\\"");
        if (part.Any(char.IsUpper)) problems.Add("capital letters in \\"" + part + "\\"");
        if (part.Contains('_')) problems.Add("underscore in \\"" + part + "\\"");
    }
    return problems;
}

foreach (string url in new[] { "/getOrdersByCustomerId?id=7", "/customers/{id}/orders", "/menu_items", "/orders/{id}" })
{
    var problems = Check(url);
    Console.WriteLine(url + " -> " + (problems.Count == 0 ? "ok" : string.Join(", ", problems)));
}`;

const verbs = ["get", "create", "update", "delete", "add", "remove", "fetch"];

function check(url: string): string[] {
  const problems: string[] = [];
  const path = url.split("?")[0];
  for (const part of path.replace(/^\\/|\\/$/g, "").split("/")) {
    if (part.startsWith("{")) continue;
    if (verbs.some((v) => part.toLowerCase().startsWith(v))) problems.push('verb in "' + part + '"');
    if (/[A-Z]/.test(part)) problems.push('capital letters in "' + part + '"');
    if (part.includes("_")) problems.push('underscore in "' + part + '"');
  }
  return problems;
}

for (const url of ["/getOrdersByCustomerId?id=7", "/customers/{id}/orders", "/menu_items", "/orders/{id}"]) {
  const problems = check(url);
  console.log(url + " -> " + (problems.length === 0 ? "ok" : problems.join(", ")));
}`;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Newest orders come first, so new orders appear at the top of the list.
var orders = new List<Order>
{
    new(106, 8, "Latte", 220, "cancelled"),
    new(105, 7, "Latte", 440, "open"),
    new(104, 9, "Blueberry Muffin", 150, "delivered"),
    new(103, 7, "Cold Coffee", 360, "open"),
    new(102, 8, "Masala Chai", 240, "open"),
    new(101, 7, "Latte", 220, "delivered")
};

// The cursor is "id:105" in base64url. It means "continue after order 105".
string Encode(int id) => Convert.ToBase64String(Encoding.UTF8.GetBytes("id:" + id)).TrimEnd('=').Replace('+', '-').Replace('/', '_');
int Decode(string cursor) => int.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(cursor.Replace('-', '+').Replace('_', '/'))).Substring(3));

app.MapGet("/orders", (int limit = 2, string? after = null) =>
{
    limit = Math.Clamp(limit, 1, 3);
    // Start right after the last order the client saw, not at a page number.
    var rest = after is null ? orders : orders.Where(o => o.Id < Decode(after)).ToList();
    var items = rest.Take(limit).ToList();
    string? nextCursor = rest.Count > limit ? Encode(items[^1].Id) : null;
    return TypedResults.Ok(new { items, nextCursor });
});

// A new order arrives while the client is reading the list.
app.MapPost("/orders", (NewOrder input) =>
{
    var order = new Order(orders[0].Id + 1, input.CustomerId, input.Item, input.Total, "open");
    orders.Insert(0, order);
    return TypedResults.Created($"/orders/{order.Id}", order);
});

app.Run();

record Order(int Id, int CustomerId, string Item, int Total, string Status);
record NewOrder(int CustomerId, string Item, int Total);`;

type Order = { id: number; customerId: number; item: string; total: number; status: string };

// Newest orders come first, so new orders appear at the top of the list.
const orders: Order[] = [
  { id: 106, customerId: 8, item: "Latte", total: 220, status: "cancelled" },
  { id: 105, customerId: 7, item: "Latte", total: 440, status: "open" },
  { id: 104, customerId: 9, item: "Blueberry Muffin", total: 150, status: "delivered" },
  { id: 103, customerId: 7, item: "Cold Coffee", total: 360, status: "open" },
  { id: 102, customerId: 8, item: "Masala Chai", total: 240, status: "open" },
  { id: 101, customerId: 7, item: "Latte", total: 220, status: "delivered" },
];

// The cursor is "id:105" in base64url. It means "continue after order 105".
const encode = (id: number) => btoa("id:" + id).replace(/=+$/, "").replace(/\\+/g, "-").replace(/\\//g, "_");
const decode = (cursor: string) => Number(atob(cursor.replace(/-/g, "+").replace(/_/g, "/")).slice(3));

const app = new Hono();

app.get("/orders", (c) => {
  const limit = Math.min(Math.max(Number(c.req.query("limit") ?? 2), 1), 3);
  const after = c.req.query("after");
  // Start right after the last order the client saw, not at a page number.
  const rest = after === undefined ? orders : orders.filter((o) => o.id < decode(after));
  const items = rest.slice(0, limit);
  const nextCursor = rest.length > limit ? encode(items[items.length - 1].id) : null;
  return c.json({ items, nextCursor }, 200);
});

// A new order arrives while the client is reading the list.
app.post("/orders", async (c) => {
  const input = await c.req.json();
  const order: Order = { id: orders[0].id + 1, customerId: input.customerId, item: input.item, total: input.total, status: "open" };
  orders.unshift(order);
  c.header("Location", \`/orders/\${order.id}\`);
  return c.json(order, 201);
});

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

{"customerId":7,"item":"Masala Chai","total":120}

###
GET /orders?limit=2&after=aWQ6MTA1

###
GET /orders?limit=2&after=aWQ6MTAz`;

  {
    prompt: "Rewrite each action-style endpoint as a method plus a REST URL, like GET /menu.",
    hint: "Take the verb out of the URL and make it the method. Put the id in the path.",
    explain: "Every action URL becomes a plural noun, an optional id, and a method.",
    csharp: { starter: `// Rewrite each action-style endpoint as a method plus a REST URL.
// Example: "/getMenu" becomes "GET /menu".
string[] rest =
{
    "GET /getMenu",
    "GET /getOrderById?id=101",
    "GET /getOrdersByCustomerId?id=7",
    "POST /deleteOrderById?id=101"
};

foreach (string line in rest) Console.WriteLine(line);`, solution: `// Rewrite each action-style endpoint as a method plus a REST URL.
// Example: "/getMenu" becomes "GET /menu".
string[] rest =
{
    "GET /menu",
    "GET /orders/101",
    "GET /customers/7/orders",
    "DELETE /orders/101"
};

foreach (string line in rest) Console.WriteLine(line);`, mode: "console" },
    react: { starter: `// Rewrite each action-style endpoint as a method plus a REST URL.
// Example: "/getMenu" becomes "GET /menu".
const rest: string[] = [
  "GET /getMenu",
  "GET /getOrderById?id=101",
  "GET /getOrdersByCustomerId?id=7",
  "POST /deleteOrderById?id=101",
];

for (const line of rest) console.log(line);`, solution: `// Rewrite each action-style endpoint as a method plus a REST URL.
// Example: "/getMenu" becomes "GET /menu".
const rest: string[] = [
  "GET /menu",
  "GET /orders/101",
  "GET /customers/7/orders",
  "DELETE /orders/101",
];

for (const line of rest) console.log(line);`, mode: "console" }
  },
  {
    prompt: "The app wants the orders of a customer. Replace /getOrdersByCustomerId with GET /customers/{customerId}/orders. The request asks for customer 8.",
    hint: "Use a route parameter for the customer id. In C#: \"/customers/{customerId:int}/orders\". In Hono: \"/customers/:customerId/orders\".",
    explain: "Orders that belong to a customer live under that customer. The id moves from the query string into the path.",
    csharp: { starter: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var orders = new List<Order>
{
    new(101, 7, "Latte", 220, "delivered"),
    new(102, 8, "Masala Chai", 240, "open"),
    new(103, 7, "Cold Coffee", 360, "open"),
    new(104, 9, "Blueberry Muffin", 150, "delivered"),
    new(105, 7, "Latte", 440, "open"),
    new(106, 8, "Latte", 220, "cancelled")
};

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

app.MapGet("/orders/{id:int}", (int id) =>
    orders.FirstOrDefault(o => o.Id == id) is { } order ? Results.Ok(order) : Results.NotFound());

// The app wants a customer's orders. Replace this action URL with
// GET /customers/{customerId}/orders.
app.MapGet("/getOrdersByCustomerId", (int id) => TypedResults.Ok(orders.Where(o => o.CustomerId == id)));

app.Run();

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

var orders = new List<Order>
{
    new(101, 7, "Latte", 220, "delivered"),
    new(102, 8, "Masala Chai", 240, "open"),
    new(103, 7, "Cold Coffee", 360, "open"),
    new(104, 9, "Blueberry Muffin", 150, "delivered"),
    new(105, 7, "Latte", 440, "open"),
    new(106, 8, "Latte", 220, "cancelled")
};

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

app.MapGet("/orders/{id:int}", (int id) =>
    orders.FirstOrDefault(o => o.Id == id) is { } order ? Results.Ok(order) : Results.NotFound());

// The app wants a customer's orders. Replace this action URL with
// GET /customers/{customerId}/orders.
app.MapGet("/customers/{customerId:int}/orders", (int customerId) =>
    TypedResults.Ok(orders.Where(o => o.CustomerId == customerId)));

app.Run();

record Order(int Id, int CustomerId, string Item, int Total, string Status);`, mode: "aspnet", requests: `GET /customers/8/orders` },
    react: { starter: `import { Hono } from "hono";

type Order = { id: number; customerId: number; item: string; total: number; status: string };

const orders: Order[] = [
  { id: 101, customerId: 7, item: "Latte", total: 220, status: "delivered" },
  { id: 102, customerId: 8, item: "Masala Chai", total: 240, status: "open" },
  { id: 103, customerId: 7, item: "Cold Coffee", total: 360, status: "open" },
  { id: 104, customerId: 9, item: "Blueberry Muffin", total: 150, status: "delivered" },
  { id: 105, customerId: 7, item: "Latte", total: 440, status: "open" },
  { id: 106, customerId: 8, item: "Latte", total: 220, status: "cancelled" },
];

const app = new Hono();

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

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

// The app wants a customer's orders. Replace this action URL with
// GET /customers/:customerId/orders.
app.get("/getOrdersByCustomerId", (c) => c.json(orders.filter((o) => o.customerId === Number(c.req.query("id"))), 200));

type Order = { id: number; customerId: number; item: string; total: number; status: string };

const orders: Order[] = [
  { id: 101, customerId: 7, item: "Latte", total: 220, status: "delivered" },
  { id: 102, customerId: 8, item: "Masala Chai", total: 240, status: "open" },
  { id: 103, customerId: 7, item: "Cold Coffee", total: 360, status: "open" },
  { id: 104, customerId: 9, item: "Blueberry Muffin", total: 150, status: "delivered" },
  { id: 105, customerId: 7, item: "Latte", total: 440, status: "open" },
  { id: 106, customerId: 8, item: "Latte", total: 220, status: "cancelled" },
];

const app = new Hono();

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

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

// The app wants a customer's orders. Replace this action URL with
// GET /customers/:customerId/orders.
app.get("/customers/:customerId/orders", (c) =>
  c.json(orders.filter((o) => o.customerId === Number(c.req.param("customerId"))), 200));

    checks: `GET /customers/7/orders

###
GET /customers/42/orders`
  },
  {
    prompt: "GET /orders?pageSize=50 sends every order. Cap the page size so that one page never has more than 3 orders.",
    hint: "Limit pageSize before you take the page. In C#, Math.Clamp(pageSize, 1, 3). In TypeScript, Math.min and Math.max.",
    explain: "A cap stops any client from requesting every row at once. The reply now says pageSize 3.",
    csharp: { starter: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var orders = new List<Order>
{
    new(101, 7, "Latte", 220, "delivered"),
    new(102, 8, "Masala Chai", 240, "open"),
    new(103, 7, "Cold Coffee", 360, "open"),
    new(104, 9, "Blueberry Muffin", 150, "delivered"),
    new(105, 7, "Latte", 440, "open"),
    new(106, 8, "Latte", 220, "cancelled")
};

// Send the list one page at a time. The client asks for a page and a size.
// Cap the page size: never send more than 3 orders in one page.
app.MapGet("/orders", (int page = 1, int pageSize = 2) =>
{
    var items = orders.Skip((page - 1) * pageSize).Take(pageSize).ToList();
    return TypedResults.Ok(new { items, page, pageSize, total = orders.Count });
});

app.Run();

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

var orders = new List<Order>
{
    new(101, 7, "Latte", 220, "delivered"),
    new(102, 8, "Masala Chai", 240, "open"),
    new(103, 7, "Cold Coffee", 360, "open"),
    new(104, 9, "Blueberry Muffin", 150, "delivered"),
    new(105, 7, "Latte", 440, "open"),
    new(106, 8, "Latte", 220, "cancelled")
};

// Send the list one page at a time. The client asks for a page and a size.
// Cap the page size: never send more than 3 orders in one page.
app.MapGet("/orders", (int page = 1, int pageSize = 2) =>
{
    pageSize = Math.Clamp(pageSize, 1, 3);
    var items = orders.Skip((page - 1) * pageSize).Take(pageSize).ToList();
    return TypedResults.Ok(new { items, page, pageSize, total = orders.Count });
});

app.Run();

record Order(int Id, int CustomerId, string Item, int Total, string Status);`, mode: "aspnet", requests: `GET /orders?pageSize=50` },
    react: { starter: `import { Hono } from "hono";

type Order = { id: number; customerId: number; item: string; total: number; status: string };

const orders: Order[] = [
  { id: 101, customerId: 7, item: "Latte", total: 220, status: "delivered" },
  { id: 102, customerId: 8, item: "Masala Chai", total: 240, status: "open" },
  { id: 103, customerId: 7, item: "Cold Coffee", total: 360, status: "open" },
  { id: 104, customerId: 9, item: "Blueberry Muffin", total: 150, status: "delivered" },
  { id: 105, customerId: 7, item: "Latte", total: 440, status: "open" },
  { id: 106, customerId: 8, item: "Latte", total: 220, status: "cancelled" },
];

const app = new Hono();

// Send the list one page at a time. The client asks for a page and a size.
// Cap the page size: never send more than 3 orders in one page.
app.get("/orders", (c) => {
  const page = Number(c.req.query("page") ?? 1);
  const pageSize = Number(c.req.query("pageSize") ?? 2);
  const items = orders.slice((page - 1) * pageSize, page * pageSize);
  return c.json({ items, page, pageSize, total: orders.length }, 200);
});

type Order = { id: number; customerId: number; item: string; total: number; status: string };

const orders: Order[] = [
  { id: 101, customerId: 7, item: "Latte", total: 220, status: "delivered" },
  { id: 102, customerId: 8, item: "Masala Chai", total: 240, status: "open" },
  { id: 103, customerId: 7, item: "Cold Coffee", total: 360, status: "open" },
  { id: 104, customerId: 9, item: "Blueberry Muffin", total: 150, status: "delivered" },
  { id: 105, customerId: 7, item: "Latte", total: 440, status: "open" },
  { id: 106, customerId: 8, item: "Latte", total: 220, status: "cancelled" },
];

const app = new Hono();

// Send the list one page at a time. The client asks for a page and a size.
// Cap the page size: never send more than 3 orders in one page.
app.get("/orders", (c) => {
  const page = Number(c.req.query("page") ?? 1);
  const pageSize = Math.min(Math.max(Number(c.req.query("pageSize") ?? 2), 1), 3);
  const items = orders.slice((page - 1) * pageSize, page * pageSize);
  return c.json({ items, page, pageSize, total: orders.length }, 200);
});

    checks: `GET /orders?page=2&pageSize=3

###
GET /orders?page=3

###
GET /orders?page=2&pageSize=0`
  }
];
