Expert · Web APIs with ASP.NET Core and EF Core · topic 32 of 43 · 35 min
Routing, model binding, validation and status codes
Explanation
Routing
A route is a URL pattern. ASP.NET Core compares each request's path and method to the routes you mapped and picks the best match. Curly braces mark route parameters, and constraints after a colon restrict them.
app.MapGet("/trains/{number}", (string number) => $"Train {number}");
app.MapGet("/orders/{id:int}", (int id) => $"Order {id}");
app.MapGet("/bookings/{date:datetime}", (DateTime date) => date.ToShortDateString());
app.MapGet("/files/{*path}", (string path) => path); // catch-all: matches a/b/c.txt
var api = app.MapGroup("/api/v1/products"); // shared prefix
api.MapGet("/", () => "all products");
api.MapGet("/{id:int}", (int id) => $"product {id}");Model binding: where parameters come from
| Source | Minimal API | Controller attribute | Example |
|---|---|---|---|
| Route | Name matches a {route} value | [FromRoute] | /orders/{id} |
| Query string | Simple type not in the route | [FromQuery] | /search?q=kurta&page=2 |
| Body (JSON) | Complex type on POST/PUT | [FromBody] | {"name":"Pen","price":10} |
| Header | [FromHeader] | [FromHeader] | X-Request-Id: abc |
| Services | Registered service type | [FromServices] | IOrderService |
app.MapGet("/search", (string q, int page = 1, int size = 20) =>
$"Searching '{q}', page {page}, size {size}");
// GET /search?q=kurta&page=2 -> Searching 'kurta', page 2, size 20
app.MapPost("/orders", (CreateOrder order) => Results.Ok(order));
// POST /orders with JSON {"productId":7,"quantity":2}
public record CreateOrder(int ProductId, int Quantity);Validation: never trust input
Anything can arrive in a request: empty names, negative quantities, a 5 MB "address". Validate at the edge, before the data touches your database. The common tool is data annotations: attributes on properties that state the rules.
using System.ComponentModel.DataAnnotations;
public class CreateProduct
{
[Required, StringLength(100, MinimumLength = 2)]
public string Name { get; set; } = "";
[Range(1, 1_000_000)]
public decimal Price { get; set; }
[EmailAddress]
public string? SupplierEmail { get; set; }
}In controllers marked [ApiController], these rules run automatically, and a bad request gets a 400 response with a ProblemDetails body listing every error, before your action runs. From .NET 10, minimal APIs can do the same after you call builder.Services.AddValidation(); on older versions you check by hand (or use a library such as FluentValidation) and return Results.ValidationProblem.
app.MapPost("/products", (CreateProduct p) =>
{
var errors = new Dictionary<string, string[]>();
if (string.IsNullOrWhiteSpace(p.Name)) errors["name"] = new[] { "Name is required" };
if (p.Price <= 0) errors["price"] = new[] { "Price must be positive" };
if (errors.Count > 0) return Results.ValidationProblem(errors);
// ... save ...
return Results.Created($"/products/1", p);
});Status codes: say what happened
| Code | Name | Use when |
|---|---|---|
| 200 | OK | Success, with data in the body |
| 201 | Created | A POST created something; include its location |
| 204 | No Content | Success with nothing to return (e.g. DELETE) |
| 400 | Bad Request | The input is invalid |
| 401 | Unauthorized | Not logged in / no valid token |
| 403 | Forbidden | Logged in but not allowed |
| 404 | Not Found | The resource does not exist |
| 409 | Conflict | Clashes with current state (e.g. seat already booked) |
| 500 | Internal Server Error | A bug or crash on the server; never send on purpose for bad input |
Common mistakes and fixes
- Using a database entity class directly as the input type: clients can set fields they should not (like IsAdmin or Id). Use small input records (DTOs, Data Transfer Objects) for requests.
- Two routes that match the same URL: an "ambiguous match" error at run time. Make routes distinct or add constraints.
- Expecting a GET to read a JSON body: GET requests should use route and query parameters.
- Validating only in the mobile app: anyone can call your API directly with tools like curl. The server must validate.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Routing, model binding, validation and status codes · 1/7
In this lesson you will control which code handles each request, where the inputs come from, and what status the client gets.
Prefer a video by a teacher? Find free videos on “ASP.NET Core routing model binding validation status codes web api” on YouTube ↗
In real life: A seat booking endpoint for a bus company
A bus booking API must reject bad input (400), unknown buses (404) and seats already taken (409), and return 201 when a booking succeeds. Clear status codes let the app show the right message: "invalid seat", "bus not found" or "someone just booked this seat".
var booked = new HashSet<(int Bus, int Seat)> { (501, 12) };
var buses = new HashSet<int> { 501, 502 };
app.MapPost("/buses/{bus:int}/bookings", (int bus, BookSeat req) =>
{
if (req.Seat is < 1 or > 40)
return Results.ValidationProblem(new Dictionary<string, string[]> { ["seat"] = new[] { "Seat must be 1 to 40" } });
if (!buses.Contains(bus))
return Results.NotFound();
if (!booked.Add((bus, req.Seat)))
return Results.Conflict(new { message = $"Seat {req.Seat} is already booked" });
return Results.Created($"/buses/{bus}/bookings/{req.Seat}", new { bus, req.Seat, req.Passenger });
});
public record BookSeat(int Seat, string Passenger);Practice (0/4 done)
GET /search?q=rice&page=3 with handler (string q, int page = 1). What is page?
A user tries to book a seat that was just booked by someone else. Best status code?
Where does a POST handler parameter of type CreateOrder (a record) usually come from in a minimal API?
Add a POST /students endpoint that accepts { "name": "...", "rollNo": 12, "email": "..." }. Reject: empty name, rollNo outside 1 to 500, and an email without "@". Return 400 with all errors together, 409 if the roll number already exists, and 201 on success.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Dependency injection and service lifetimesWeb APIs with ASP.NET Core and EF Core · same module
- Configuration, secrets and the middleware pipelineWeb APIs with ASP.NET Core and EF Core · same module
- Routing: sending each request to the right codeBackend Development · APIs and request handling
- REST API design and JSON handlersGo Programming (backend and cloud) · Building a REST API with a database
- Validating inputBackend Development · APIs and request handling
- Route handlers that follow the contractFull-Stack Development · Building the backend
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What is the difference between 401 and 403?
Answer: 401: not authenticated; 403: authenticated but not allowed
Unauthorized means "who are you?"; Forbidden means "you may not do this".
What does [ApiController] do for invalid model state?
Answer: Automatically returns 400 with the validation errors
Automatic 400 responses are part of [ApiController].
Why not accept your database entity class directly as API input?
Answer: Clients could set fields they should not, such as Id or IsAdmin
Use dedicated input DTOs to avoid over-posting.
Which status is right for a successful DELETE with nothing to return?
Answer: 204
204 No Content.
What does MapGroup("/api/v1/products") help with?
Answer: Sharing a route prefix (and settings) across related endpoints
Route groups reduce repetition.
Which route handles GET /products?sort=price?Related
Answer: GET /products
The query string is not part of routing. The handler of GET /products reads it.
Why make handlers depend on a Store interface?Related
Answer: The storage can change and tests can use a fake without touching handlers
This is the "accept interfaces" idea applied to APIs.
The JSON is fine, but age is -4. Which status fits best?Related
Answer: 422 Unprocessable Content
The request is well-formed, but a value breaks a rule. Many APIs also use 400 for this.
From Validating input
In Express 5, what happens when an async route handler throws an error?Related
Answer: Express passes the error to the error-handling middleware automatically
Express 5 catches rejected promises from handlers and calls next(err) for you. Express 4 needed try/catch or a wrapper.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 3 more topics in “Web APIs with ASP.NET Core and EF Core” before its exam: Dependency injection and service lifetimes, Configuration, secrets and the middleware pipeline, Entity Framework Core: databases with C#.
- Module 8 exam: Web APIs with ASP.NET Core and EF Core · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Testing, tooling and Unity. 43 topics left in the course before the final exam.