# Request Validation and DTOs

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

> Stop bad data at the door: use request and response DTOs, block over-posting, validate every field, and return every error as a validation problem.

**Validation** means checking incoming data before the API accepts and uses it. A **DTO (Data Transfer Object)** is a simple class or type that defines exactly what data is allowed to enter or leave the API. It acts as a filter between the client and the server.

Together, DTOs and validation help protect the API from incorrect, missing, or unwanted data. Instead of trusting everything the client sends, the API checks the data first and accepts only what follows the rules.

## Watch Bad Data Get Saved

The API accepts and saves everything that the client sends. Press **Run**. The Requests box sends three orders that should never be accepted by a real application.

- Order `101` has a quantity of `-2`, which creates a total of `-440`.
- Order `102` sends its own price of `1`, so two lattes cost only ₹2.
- Order `103` does not even include an item name.

All three orders receive `201 Created`.

**In simple words:** the API trusted the client completely. This is dangerous because anyone can send any JSON they want. In this lesson, you will learn how to check requests before accepting them and allow only the fields and values that are valid.

## TL;DR

- **Never trust the client.** Anyone can send requests directly to your API.
- A **request DTO** defines what data a client is allowed to send.
- A **response DTO** defines what data a client is allowed to receive.
- Never allow clients to set server-owned values such as `id`, `price`, or `total`.
- Accepting fields that should not be accepted is called **over-posting**.
- Validate required fields, ranges, and allowed values.
- Return all validation errors together instead of stopping at the first error.
- Return `400 Bad Request` when validation fails.
- In C#, attributes such as `[Required]` and `[Range]` can define validation rules directly on the DTO.

## A Real-World Example

Think about filling out a form at a bank. The form only contains fields that customers are allowed to complete, such as:

- Name
- Account number
- Amount

There is no field for information that only the bank controls. Before accepting the form, the bank employee checks it to make sure:

- Required details are provided.
- Numbers are valid.
- The information follows all rules.

If something is wrong, the employee points out every problem before processing the form. A request DTO is like that form, and validation is like that review process.

## What You Will Learn

In this lesson, you will learn:

1. Why every API request should be validated.
2. What request and response DTOs are.
3. How DTOs prevent over-posting.
4. How to validate requests and return multiple errors.
5. How data annotations simplify validation in C#.
6. How to test an API using invalid input.

## Request and Response DTOs

The first step is controlling the shape of the data. The server uses its own internal `Order` model. The client only sends a small request DTO such as: **CreateOrderRequest**

which contains:

- `item`
- `quantity`

The server calculates everything else itself. The response is returned through a separate DTO such as: **OrderResponse**

which only contains the information that the client needs.

The client can no longer control fields such as:

- `price`
- `total`
- `id`

Even if those values are sent, the request DTO ignores them because they are not part of the allowed input. The server now decides all prices and calculates all totals itself.

In C#, a DTO can be as simple as `record CreateOrderRequest(string Item, int Quantity);`. A record is a concise way to create a class that only stores data. In TypeScript, DTOs are commonly defined using `type` or `interface`.

## Validate Every Field

A DTO controls **which fields** can be sent. Validation controls **what values those fields may contain**.

The first request contains two problems:

- Invalid item name
- Invalid quantity

The response reports both issues. The second request is missing the item name. The third request is valid and receives `201 Created`. Returning all errors together helps users fix everything in a single attempt.

Collect all errors before you answer. If you stop at the first one, the customer fixes it, sends again, and only then hears about the second. That is slow and annoying.

## Rules on the DTO: Data Annotations

Writing many `if` statements works, but it becomes difficult to manage as applications grow. In C#, validation rules can be placed directly on DTO properties using **data annotations**.

Examples include:

```csharp
[Required]
[StringLength(40, MinimumLength = 2)]
[EmailAddress]
[Range(13, 120)]
```

These attributes describe the validation rules directly in the DTO.

In this example:

- The first request breaks three validation rules.
- The response includes all three validation errors.
- The second request passes validation successfully.

In TypeScript, the same validation rules are often implemented using libraries such as **Zod**.

## Test with Bad Data

A good way to verify validation is to intentionally send invalid data. This example sends several bad requests and then checks what was saved.

All five requests receive `400 Bad Request`. One request even attempts to send SQL code as part of the item name.

The final request returns: []

No invalid order was saved. This proves that the validation rules are working correctly.

**Validate on the server, always.** Checks in the app are nice for the customer, but anyone can skip them by calling the API directly. Only the server's checks protect your data.

## Where You See This

- **Registration forms** show field-specific validation messages.
- **Payment systems** reject invalid or negative amounts before processing.
- **Order systems** validate quantities before creating orders.
- **ASP.NET Core controllers** automatically validate data annotations when using `[ApiController]`.

## Common mistakes

| Symptom | Cause | Fix |
|---|---|---|
| Orders with negative totals | There is no range check on quantity | Check `1 <= quantity <= 10` |
| Customers set their own price | The request type has a `price` field (over-posting) | Remove it from the request DTO, and look the price up |
| Users fix one error, and then see another | Validation stops at the first error | Collect all errors, and then answer |
| Internal fields appear in replies | The full database class is returned | Return a response DTO |

## Exercises

## What to Remember

- Never trust the client. Check every request on the server.
- A **request DTO** lists what may come in. A **response DTO** lists what may go out.
- The server works out prices, totals and ids. Never accept them from the client.
- Return `400` with **every** error, named by field.
- Attack your own API with bad data, to prove that the checks work.

**Next:** Building a CRUD API. You will put it all together: create, read, update and delete menu items and orders.

var app = builder.Build();

var orders = new List<Order>();

// No checks at all. Whatever arrives is saved.
app.MapPost("/orders", (Order input) =>
{
    var order = input with { Id = orders.Count + 101 };
    orders.Add(order);
    return Results.Created("/orders/" + order.Id, order);
});

app.Run();

// The client sends the whole Order, including the price.
record Order(int Id, string Item, int Quantity, int Price, int Total);`;

// The client sends the whole Order, including the price.
type Order = { id: number; item: string; quantity: number; price: number; total: number };

const orders: Order[] = [];
const app = new Hono();

// No checks at all. Whatever arrives is saved.
app.post("/orders", async (c) => {
  const input = await c.req.json<Order>();
  const order = { ...input, id: orders.length + 101 };
  orders.push(order);
  c.header("Location", "/orders/" + order.id);
  return c.json(order, 201);
});

Content-Type: application/json

{"item":"Latte","quantity":-2,"price":220,"total":-440}

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

{"item":"Latte","quantity":2,"price":1,"total":2}

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

{"quantity":1}`;

var app = builder.Build();

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

// The request DTO has only what the client is allowed to send.
// The server looks up the price itself.
app.MapPost("/orders", (CreateOrderRequest input) =>
{
    int price = menu[input.Item];
    var order = new Order(orders.Count + 101, input.Item, input.Quantity, price * input.Quantity, "kitchen note: rush");
    orders.Add(order);

    // The response DTO has only what the client should see.
    var reply = new OrderResponse(order.Id, order.Item, order.Quantity, order.Total);
    return Results.Created("/orders/" + order.Id, reply);
});

app.Run();

// What the client sends: no id, no price, no total.
record CreateOrderRequest(string Item, int Quantity);

// What the database keeps: includes an internal note.
record Order(int Id, string Item, int Quantity, int Total, string InternalNote);

// What the client gets back: no internal note.
record OrderResponse(int Id, string Item, int Quantity, int Total);`;

const menu = new Map<string, number>([["Latte", 220], ["Masala Chai", 120]]);

// What the client sends: no id, no price, no total.
type CreateOrderRequest = { item: string; quantity: number };

// What the database keeps: includes an internal note.
type Order = { id: number; item: string; quantity: number; total: number; internalNote: string };

// What the client gets back: no internal note.
type OrderResponse = { id: number; item: string; quantity: number; total: number };

const orders: Order[] = [];
const app = new Hono();

app.post("/orders", async (c) => {
  const body = await c.req.json<CreateOrderRequest>();
  // Copy only the allowed fields. Anything else in the body is ignored.
  const input: CreateOrderRequest = { item: body.item, quantity: body.quantity };
  const price = menu.get(input.item)!;
  const order: Order = { id: orders.length + 101, item: input.item, quantity: input.quantity, total: price * input.quantity, internalNote: "kitchen note: rush" };
  orders.push(order);

  const reply: OrderResponse = { id: order.id, item: order.item, quantity: order.quantity, total: order.total };
  c.header("Location", "/orders/" + order.id);
  return c.json(reply, 201);
});

Content-Type: application/json

{"item":"Latte","quantity":2,"price":1,"total":2}`;

var app = builder.Build();

var menu = new Dictionary<string, int> { ["Latte"] = 220, ["Masala Chai"] = 120 };
var orders = new List<OrderResponse>();

// Check every field. Collect all problems, not just the first one.
Dictionary<string, string[]> Validate(CreateOrderRequest input)
{
    var errors = new Dictionary<string, string[]>();
    if (string.IsNullOrWhiteSpace(input.Item))
        errors["item"] = new[] { "Item is required." };
    else if (!menu.ContainsKey(input.Item))
        errors["item"] = new[] { "We do not sell " + input.Item + "." };
    if (input.Quantity < 1 || input.Quantity > 10)
        errors["quantity"] = new[] { "Quantity must be between 1 and 10." };
    return errors;
}

app.MapPost("/orders", (CreateOrderRequest input) =>
{
    var errors = Validate(input);
    if (errors.Count > 0) return Results.ValidationProblem(errors);

    var order = new OrderResponse(orders.Count + 101, input.Item!, input.Quantity, menu[input.Item!] * input.Quantity);
    orders.Add(order);
    return Results.Created("/orders/" + order.Id, order);
});

app.Run();

record CreateOrderRequest(string? Item, int Quantity);
record OrderResponse(int Id, string Item, int Quantity, int Total);`;

const menu = new Map<string, number>([["Latte", 220], ["Masala Chai", 120]]);
type CreateOrderRequest = { item?: string; quantity?: number };
type OrderResponse = { id: number; item: string; quantity: number; total: number };
const orders: OrderResponse[] = [];

// Check every field. Collect all problems, not just the first one.
function validate(input: CreateOrderRequest): Record<string, string[]> {
  const errors: Record<string, string[]> = {};
  if (!input.item || input.item.trim() === "") errors.item = ["Item is required."];
  else if (!menu.has(input.item)) errors.item = ["We do not sell " + input.item + "."];
  const q = input.quantity ?? 0;
  if (q < 1 || q > 10) errors.quantity = ["Quantity must be between 1 and 10."];
  return errors;
}

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

const app = new Hono();

app.post("/orders", async (c) => {
  const input = await c.req.json<CreateOrderRequest>();
  const errors = validate(input);
  if (Object.keys(errors).length > 0) return validationProblem(c, errors);

  const order: OrderResponse = { id: orders.length + 101, item: input.item!, quantity: input.quantity!, total: menu.get(input.item!)! * input.quantity! };
  orders.push(order);
  c.header("Location", "/orders/" + order.id);
  return c.json(order, 201);
});

Content-Type: application/json

{"item":"Pizza","quantity":50}

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

{"quantity":2}

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

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

using System.Text.Json;

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

// The rules live on the DTO itself, as attributes.
app.MapPost("/customers", (CreateCustomerRequest input) =>
{
    var results = new List<ValidationResult>();
    bool ok = Validator.TryValidateObject(input, new ValidationContext(input), results, validateAllProperties: true);
    if (!ok)
    {
        var errors = results
            .GroupBy(r => JsonNamingPolicy.CamelCase.ConvertName(r.MemberNames.First()))
            .ToDictionary(g => g.Key, g => g.Select(r => r.ErrorMessage!).ToArray());
        return Results.ValidationProblem(errors);
    }
    return Results.Created("/customers/7", new { id = 7, input.Name, input.Email });
});

app.Run();

record CreateCustomerRequest(
    [property: Required, StringLength(40, MinimumLength = 2)] string? Name,
    [property: Required, EmailAddress] string? Email,
    [property: Range(13, 120)] int Age);`;

type CreateCustomerRequest = { name?: string; email?: string; age?: number };

// The rules live in one schema object, one rule list per field.
// (Real projects often use a library such as zod for this.)
type Rule = (value: unknown) => string | null;
const schema: Record<string, Rule[]> = {
  name: [
    (v) => (typeof v === "string" && v.trim() !== "" ? null : "The Name field is required."),
    (v) => (typeof v !== "string" || (v.length >= 2 && v.length <= 40) ? null : "The field Name must be a string with a minimum length of 2 and a maximum length of 40."),
  ],
  email: [
    (v) => (typeof v === "string" && v.trim() !== "" ? null : "The Email field is required."),
    (v) => (typeof v !== "string" || /^[^@\\s]+@[^@\\s]+$/.test(v) ? null : "The Email field is not a valid e-mail address."),
  ],
  age: [(v) => (typeof v === "number" && v >= 13 && v <= 120 ? null : "The field Age must be between 13 and 120.")],
};

function validate(input: Record<string, unknown>): Record<string, string[]> {
  const errors: Record<string, string[]> = {};
  for (const [field, rules] of Object.entries(schema)) {
    for (const rule of rules) {
      const message = rule(input[field]);
      if (message) {
        errors[field] = [message];
        break;
      }
    }
  }
  return errors;
}

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

const app = new Hono();

app.post("/customers", async (c) => {
  const input = await c.req.json<CreateCustomerRequest>();
  const errors = validate(input);
  if (Object.keys(errors).length > 0) return validationProblem(c, errors);
  c.header("Location", "/customers/7");
  return c.json({ id: 7, name: input.name, email: input.email }, 201);
});

Content-Type: application/json

{"name":"A","email":"asha-at-example","age":9}

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

{"name":"Asha","email":"asha@example.com","age":24}`;

var app = builder.Build();

var menu = new Dictionary<string, int> { ["Latte"] = 220, ["Masala Chai"] = 120 };
var orders = new List<OrderResponse>();

// The same validation as before. Now a "fuzzer" throws bad orders at it.
Dictionary<string, string[]> Validate(CreateOrderRequest input)
{
    var errors = new Dictionary<string, string[]>();
    if (string.IsNullOrWhiteSpace(input.Item))
        errors["item"] = new[] { "Item is required." };
    else if (!menu.ContainsKey(input.Item))
        errors["item"] = new[] { "We do not sell " + input.Item + "." };
    if (input.Quantity < 1 || input.Quantity > 10)
        errors["quantity"] = new[] { "Quantity must be between 1 and 10." };
    return errors;
}

app.MapPost("/orders", (CreateOrderRequest input) =>
{
    var errors = Validate(input);
    if (errors.Count > 0) return Results.ValidationProblem(errors);

    var order = new OrderResponse(orders.Count + 101, input.Item!, input.Quantity, menu[input.Item!] * input.Quantity);
    orders.Add(order);
    return Results.Created("/orders/" + order.Id, order);
});

// Read back everything that was saved.
app.MapGet("/orders", () => TypedResults.Ok(orders));

app.Run();

record CreateOrderRequest(string? Item, int Quantity);
record OrderResponse(int Id, string Item, int Quantity, int Total);`;

const menu = new Map<string, number>([["Latte", 220], ["Masala Chai", 120]]);
type CreateOrderRequest = { item?: string; quantity?: number };
type OrderResponse = { id: number; item: string; quantity: number; total: number };
const orders: OrderResponse[] = [];

// The same validation as before. Now a "fuzzer" throws bad orders at it.
function validate(input: CreateOrderRequest): Record<string, string[]> {
  const errors: Record<string, string[]> = {};
  if (!input.item || input.item.trim() === "") errors.item = ["Item is required."];
  else if (!menu.has(input.item)) errors.item = ["We do not sell " + input.item + "."];
  const q = input.quantity ?? 0;
  if (q < 1 || q > 10) errors.quantity = ["Quantity must be between 1 and 10."];
  return errors;
}

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

const app = new Hono();

app.post("/orders", async (c) => {
  const input = await c.req.json<CreateOrderRequest>();
  const errors = validate(input);
  if (Object.keys(errors).length > 0) return validationProblem(c, errors);

  const order: OrderResponse = { id: orders.length + 101, item: input.item!, quantity: input.quantity!, total: menu.get(input.item!)! * input.quantity! };
  orders.push(order);
  c.header("Location", "/orders/" + order.id);
  return c.json(order, 201);
});

// Read back everything that was saved.
app.get("/orders", (c) => c.json(orders, 200));

Content-Type: application/json

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

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

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

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

{"item":"","quantity":1}

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

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

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

{"item":"Latte; DROP TABLE orders","quantity":1}

###
GET /orders`;

  {
    prompt: "Write Check(name, phone). Return \"ok\", or every problem joined by \", \": \"name is required\" and \"phone must have 10 digits\".",
    hint: "Collect the messages in a list. Check each rule. Return \"ok\" when the list is empty.",
    explain: "Collecting every problem lets the user fix them all at once.",
    csharp: { starter: `// Return the problems with a sign-up, or "ok" if there are none.
// Rules: name is not empty. phone has exactly 10 digits.
static string Check(string name, string phone)
{
    return "ok";
}

Console.WriteLine(Check("Asha", "9840111111"));
Console.WriteLine(Check("", "9840111111"));
Console.WriteLine(Check("Ravi", "98401"));
Console.WriteLine(Check("", "abc"));`, solution: `// Return the problems with a sign-up, or "ok" if there are none.
// Rules: name is not empty. phone has exactly 10 digits.
static string Check(string name, string phone)
{
    var problems = new List<string>();
    if (string.IsNullOrWhiteSpace(name)) problems.Add("name is required");
    if (phone.Length != 10 || !phone.All(char.IsDigit)) problems.Add("phone must have 10 digits");
    return problems.Count == 0 ? "ok" : string.Join(", ", problems);
}

Console.WriteLine(Check("Asha", "9840111111"));
Console.WriteLine(Check("", "9840111111"));
Console.WriteLine(Check("Ravi", "98401"));
Console.WriteLine(Check("", "abc"));`, mode: "console" },
    react: { starter: `// Return the problems with a sign-up, or "ok" if there are none.
// Rules: name is not empty. phone has exactly 10 digits.
function check(name: string, phone: string): string {
  return "ok";
}

console.log(check("Asha", "9840111111"));
console.log(check("", "9840111111"));
console.log(check("Ravi", "98401"));
console.log(check("", "abc"));`, solution: `// Return the problems with a sign-up, or "ok" if there are none.
// Rules: name is not empty. phone has exactly 10 digits.
function check(name: string, phone: string): string {
  const problems: string[] = [];
  if (name.trim() === "") problems.push("name is required");
  if (!/^\\d{10}$/.test(phone)) problems.push("phone must have 10 digits");
  return problems.length === 0 ? "ok" : problems.join(", ");
}

console.log(check("Asha", "9840111111"));
console.log(check("", "9840111111"));
console.log(check("Ravi", "98401"));
console.log(check("", "abc"));`, mode: "console" }
  },
  {
    prompt: "Add a rule: quantity must be between 1 and 10. Otherwise, return a 400 validation problem with errors.quantity = \"Quantity must be between 1 and 10.\"",
    hint: "Add the check next to the item check, and add the message to the errors dictionary under \"quantity\".",
    explain: "Range checks stop negative and silly totals before they reach the data.",
    csharp: { starter: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var menu = new Dictionary<string, int> { ["Latte"] = 220, ["Masala Chai"] = 120 };
var orders = new List<OrderResponse>();

// The item check is done. Add the quantity rule: it must be between 1 and 10,
// with the message "Quantity must be between 1 and 10."
Dictionary<string, string[]> Validate(CreateOrderRequest input)
{
    var errors = new Dictionary<string, string[]>();
    if (string.IsNullOrWhiteSpace(input.Item))
        errors["item"] = new[] { "Item is required." };
    else if (!menu.ContainsKey(input.Item))
        errors["item"] = new[] { "We do not sell " + input.Item + "." };
    return errors;
}

app.MapPost("/orders", (CreateOrderRequest input) =>
{
    var errors = Validate(input);
    if (errors.Count > 0) return Results.ValidationProblem(errors);

    var order = new OrderResponse(orders.Count + 101, input.Item!, input.Quantity, menu[input.Item!] * input.Quantity);
    orders.Add(order);
    return Results.Created("/orders/" + order.Id, order);
});

app.Run();

record CreateOrderRequest(string? Item, int Quantity);
record OrderResponse(int Id, string Item, int Quantity, int Total);`, solution: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var menu = new Dictionary<string, int> { ["Latte"] = 220, ["Masala Chai"] = 120 };
var orders = new List<OrderResponse>();

// The item check is done. Add the quantity rule: it must be between 1 and 10,
// with the message "Quantity must be between 1 and 10."
Dictionary<string, string[]> Validate(CreateOrderRequest input)
{
    var errors = new Dictionary<string, string[]>();
    if (string.IsNullOrWhiteSpace(input.Item))
        errors["item"] = new[] { "Item is required." };
    else if (!menu.ContainsKey(input.Item))
        errors["item"] = new[] { "We do not sell " + input.Item + "." };
    if (input.Quantity < 1 || input.Quantity > 10)
        errors["quantity"] = new[] { "Quantity must be between 1 and 10." };
    return errors;
}

app.MapPost("/orders", (CreateOrderRequest input) =>
{
    var errors = Validate(input);
    if (errors.Count > 0) return Results.ValidationProblem(errors);

    var order = new OrderResponse(orders.Count + 101, input.Item!, input.Quantity, menu[input.Item!] * input.Quantity);
    orders.Add(order);
    return Results.Created("/orders/" + order.Id, order);
});

app.Run();

record CreateOrderRequest(string? Item, int Quantity);
record OrderResponse(int Id, string Item, int Quantity, int Total);`, mode: "aspnet", requests: `POST /orders
Content-Type: application/json

{"item":"Latte","quantity":0}` },
    react: { starter: `import { Hono } from "hono";

const menu = new Map<string, number>([["Latte", 220], ["Masala Chai", 120]]);
type CreateOrderRequest = { item?: string; quantity?: number };
type OrderResponse = { id: number; item: string; quantity: number; total: number };
const orders: OrderResponse[] = [];

// The item check is done. Add the quantity rule: it must be between 1 and 10,
// with the message "Quantity must be between 1 and 10."
function validate(input: CreateOrderRequest): Record<string, string[]> {
  const errors: Record<string, string[]> = {};
  if (!input.item || input.item.trim() === "") errors.item = ["Item is required."];
  else if (!menu.has(input.item)) errors.item = ["We do not sell " + input.item + "."];
  return errors;
}

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

const app = new Hono();

app.post("/orders", async (c) => {
  const input = await c.req.json<CreateOrderRequest>();
  const errors = validate(input);
  if (Object.keys(errors).length > 0) return validationProblem(c, errors);

  const order: OrderResponse = { id: orders.length + 101, item: input.item!, quantity: input.quantity!, total: menu.get(input.item!)! * input.quantity! };
  orders.push(order);
  c.header("Location", "/orders/" + order.id);
  return c.json(order, 201);
});

const menu = new Map<string, number>([["Latte", 220], ["Masala Chai", 120]]);
type CreateOrderRequest = { item?: string; quantity?: number };
type OrderResponse = { id: number; item: string; quantity: number; total: number };
const orders: OrderResponse[] = [];

// The item check is done. Add the quantity rule: it must be between 1 and 10,
// with the message "Quantity must be between 1 and 10."
function validate(input: CreateOrderRequest): Record<string, string[]> {
  const errors: Record<string, string[]> = {};
  if (!input.item || input.item.trim() === "") errors.item = ["Item is required."];
  else if (!menu.has(input.item)) errors.item = ["We do not sell " + input.item + "."];
  const q = input.quantity ?? 0;
  if (q < 1 || q > 10) errors.quantity = ["Quantity must be between 1 and 10."];
  return errors;
}

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

const app = new Hono();

app.post("/orders", async (c) => {
  const input = await c.req.json<CreateOrderRequest>();
  const errors = validate(input);
  if (Object.keys(errors).length > 0) return validationProblem(c, errors);

  const order: OrderResponse = { id: orders.length + 101, item: input.item!, quantity: input.quantity!, total: menu.get(input.item!)! * input.quantity! };
  orders.push(order);
  c.header("Location", "/orders/" + order.id);
  return c.json(order, 201);
});

Content-Type: application/json

{"item":"Latte","quantity":0}` },
    checks: `POST /orders
Content-Type: application/json

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

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

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

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

{"item":"Espresso","quantity":0}`
  },
  {
    prompt: "A customer sent \"price\": 1 and paid ₹2 for two lattes. Remove the price from the request DTO, and use the menu price.",
    hint: "Delete the Price field from the request type. Look the price up in the menu dictionary.",
    explain: "The server owns prices and totals. A field that is not in the request DTO cannot be over-posted.",
    csharp: { starter: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var menu = new Dictionary<string, int> { ["Latte"] = 220, ["Masala Chai"] = 120 };

// A customer sent "price": 1 and paid ₹2 for two lattes.
// Remove Price from the request DTO and use the menu price instead.
app.MapPost("/orders", (CreateOrderRequest input) =>
{
    int total = input.Price * input.Quantity;
    return Results.Created("/orders/101", new { id = 101, input.Item, input.Quantity, total });
});

app.Run();

record CreateOrderRequest(string Item, int Quantity, int Price);`, solution: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var menu = new Dictionary<string, int> { ["Latte"] = 220, ["Masala Chai"] = 120 };

// A customer sent "price": 1 and paid ₹2 for two lattes.
// Remove Price from the request DTO and use the menu price instead.
app.MapPost("/orders", (CreateOrderRequest input) =>
{
    int total = menu[input.Item] * input.Quantity;
    return Results.Created("/orders/101", new { id = 101, input.Item, input.Quantity, total });
});

app.Run();

record CreateOrderRequest(string Item, int Quantity);`, mode: "aspnet", requests: `POST /orders
Content-Type: application/json

{"item":"Latte","quantity":2,"price":1}` },
    react: { starter: `import { Hono } from "hono";

const menu = new Map<string, number>([["Latte", 220], ["Masala Chai", 120]]);

// A customer sent "price": 1 and paid ₹2 for two lattes.
// Remove price from the request type and use the menu price instead.
type CreateOrderRequest = { item: string; quantity: number; price: number };

const app = new Hono();

app.post("/orders", async (c) => {
  const input = await c.req.json<CreateOrderRequest>();
  const total = input.price * input.quantity;
  c.header("Location", "/orders/101");
  return c.json({ id: 101, item: input.item, quantity: input.quantity, total }, 201);
});

const menu = new Map<string, number>([["Latte", 220], ["Masala Chai", 120]]);

// A customer sent "price": 1 and paid ₹2 for two lattes.
// Remove price from the request type and use the menu price instead.
type CreateOrderRequest = { item: string; quantity: number };

const app = new Hono();

app.post("/orders", async (c) => {
  const input = await c.req.json<CreateOrderRequest>();
  const total = menu.get(input.item)! * input.quantity;
  c.header("Location", "/orders/101");
  return c.json({ id: 101, item: input.item, quantity: input.quantity, total }, 201);
});

Content-Type: application/json

{"item":"Latte","quantity":2,"price":1}` },
    checks: `POST /orders
Content-Type: application/json

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

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

{"item":"Latte","quantity":1,"price":0}`
  }
];
