Rest Apis
By the end of this lesson you'll be able to design a REST API the way professionals do — modelling resources , mapping each HTTP verb to the right action, returning correct status codes , shaping data with DTOs , and choosing between minimal APIs and controllers . You'll practise the underlying logic in the runner, then read production-ready ASP.NET Core worked examples.
Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
💡 Real-World Analogy
A REST API is like a restaurant menu of resources . The menu lists the things you can ask for — dishes, drinks, the bill — and these are your resources ( /api/orders , /api/products ). The HTTP verbs are the actions you take with each one: GET reads the menu (show me the dishes), POST places an order (create something new), PUT changes your order, and DELETE cancels it. The kitchen always replies with a status code — "here you go" (200), "your order is in" (201), "we don't have that" (404). You never walk into the kitchen and grab a pan yourself; you go through the waiter (the API), and the menu is the same for every customer (a uniform interface).
📊 HTTP Verbs & Status Codes
Verb
Means
Success code
Example route
GET
Read
200 OK
GET /api/products
POST
Create
201 Created
POST /api/products
PUT
Update / replace
PUT /api/products/1
DELETE
Remove
204 No Content
DELETE /api/products/1
Status code
Meaning
When to return it
Success, here's data
A successful GET or PUT
New resource made
A successful POST (add a Location header)
Success, nothing to send
A successful DELETE
400 Bad Request
Client sent bad input
Validation failed
404 Not Found
No such resource
The id doesn't exist
1. What "REST" Actually Means
REST (Representational State Transfer) is a set of conventions for building web APIs around resources — the nouns your system is about, like products, orders, or users. Each resource gets a URL ( /api/products/1 ), and you act on it with the standard HTTP verbs instead of inventing your own. Two ideas matter most. First, statelessness : every request carries everything the server needs, so the server doesn't remember you between calls — that's what lets an API scale to thousands of servers. Second, a uniform interface : GET always reads and never changes data, POST always creates, and so on, so any developer can guess how your API behaves. Get the nouns and verbs right and the rest of the design follows.
2. Resources, Verbs & Status Codes (Worked Example)
Here's a complete minimal API exposing a /api/todos resource. Read each endpoint and the // ✅ Expected output comment beside it — that comment is the HTTP response the call returns. Notice how each verb maps to one action and one success status code, and how a missing resource always returns 404 Not Found . This is the whole CRUD shape in one file.
A minimal API for a Todo resource. The comments show the exact HTTP response for each call.
This needs a web server, so it won't run in the browser runner — but you'll model its logic in the runnable exercise just below.
Now you try the logic behind it in plain C# — exactly what each endpoint decides before it sends a response. Fill in the ___ blanks, then run it.
3. Minimal APIs vs Controllers
ASP.NET Core gives you two styles. Minimal APIs (the example above) define endpoints with a line each — app.MapGet(...) , app.MapPost(...) — with almost no ceremony, which is perfect for microservices and small services. Controllers group related endpoints into a class decorated with attributes like [HttpGet] and [Route] ; they scale better for large APIs because they centralise routing, model binding, filters, and validation. They're equally "REST" — the difference is organisation, not behaviour. Reach for minimal APIs when an app is small or focused, and controllers when it grows many endpoints that share cross-cutting concerns.
The same kind of API as a controller — with model binding, DTOs, and versioning in the route.
Read the [FromQuery] / [FromBody] attributes — that's model binding, covered next.
4. Model Binding & DTOs
Model binding is ASP.NET Core reading values out of the incoming request and handing them to your method as typed parameters. {id} in the route binds to an int id parameter; [FromQuery] reads the query string ( ?minPrice=100 ); [FromBody] deserialises the JSON request body into an object. A DTO (Data Transfer Object) is the dedicated shape for that data — one DTO for what the client may send ( CreateProductDto ) and one for what you send back ( ProductDto ). DTOs matter because your internal entity might have fields you must never expose (a password hash, an internal cost) or never let the client set (the Id , an IsAdmin flag). Records make DTOs a one-liner. Practise building one now.
🔎 Deep Dive: Versioning Your API
Once real clients depend on your API, you can't freely change its shape — renaming a field or removing one breaks them. Versioning lets you ship breaking changes under a new version while old clients keep using the old one. The simplest, most common approach is to put the version in the URL path:
Add /v1/ from day one — it's far easier than retrofitting it later. Other strategies include a header ( api-version: 2 ) or a query string ( ?api-version=2 ); the official Asp.Versioning package supports all three. Whichever you pick, the rule is the same: never silently break an existing version.
Pro Tips
- 💡 Use TypedResults in minimal APIs: TypedResults.Ok(data) is compile-time checked and produces better OpenAPI docs than Results.Ok(data) .
- 💡 Always return a DTO, never the entity: it stops internal fields leaking and decouples your API from your database schema.
- 💡 POST returns 201 with a Location header: CreatedAtAction(...) tells the client the URL of the thing it just made.
- 💡 Keep verbs honest: a GET must never change data. If it does, it should be a POST — caches and crawlers assume GET is safe.
- 💡 Add /v1/ to your routes on day one so future breaking changes have somewhere to live.
Common Errors (and the fix)
- Returning 200 for everything: sending 200 OK on a create or a not-found hides what happened. Use 201 Created for POST, 204 No Content for DELETE, and 404 Not Found when the id doesn't exist.
- Over-posting / mass assignment: binding the request straight onto your entity lets a client set fields they shouldn't (like Id or IsAdmin ). Bind to a narrow input DTO that only contains the fields they're allowed to send.
- Not validating input: trusting the body leads to bad data and crashes. With [ApiController] , Data Annotations like [Required] and [Range] auto-return 400 Bad Request before your code runs — use them.
- Returning entities instead of DTOs: serialising your database entity can leak sensitive columns and ties your public contract to your storage. Map to a ProductDto and return that.
- "404" when the route is wrong: a typo like /api/product/1 (singular) won't match /api/products/ {id} . Check the controller's [Route] and the verb attribute match the URL you're calling.
📋 Quick Reference
Task
Minimal API
Controller
Read a list
app.MapGet("/items", ...)
[HttpGet]
app.MapPost("/items", ...)
[HttpPost]
Read from body
(Dto dto) => ...
[FromBody] Dto dto
Return 200
Results.Ok(x)
return Ok(x);
Return 201
Results.Created(url, x)
CreatedAtAction(...)
Return 404
Results.NotFound()
return NotFound();
Return 204
Results.NoContent()
return NoContent();
Frequently Asked Questions
Q: What's the difference between PUT and POST?
POST creates a new resource and the server assigns its id ( 201 Created ). PUT replaces an existing resource at a known URL ( 200 OK , or 404 if it doesn't exist). Rule of thumb: POST to a collection ( /api/todos ), PUT to a specific item ( /api/todos/1 ).
Both are first-class and equally RESTful. Use minimal APIs for small or focused services where less ceremony wins; use controllers when an API grows many endpoints that share routing, filters, and validation. You can even mix them in one project.
Q: Why bother with a DTO instead of returning my entity?
A DTO is your public contract. Returning the entity ties your API to your database and risks leaking fields you never meant to expose. A DTO lets you change storage freely and control exactly what goes over the wire.
400 Bad Request means the client sent something invalid (a missing field, a bad value). 404 Not Found means the request was fine but the resource doesn't exist (no product with that id). Validation failures are 400; missing ids are 404.
The server keeps no memory of you between requests — each call carries everything it needs (such as an auth token). That's what lets you put many identical servers behind a load balancer, because any of them can handle any request.
Mini-Challenge: Model a Notes Resource
No blanks this time — just a brief and an outline. Model an in-memory /api/notes resource over a List<Note> , with Get , Post , and Delete methods that return the same status strings a real REST API would. This is the runner-friendly version of everything you read in the worked examples. Run it and check your output against the comments.
🎉 Lesson Complete
- ✅ REST models your system as resources with a uniform, stateless interface
- ✅ Verbs map to actions: GET read, POST create, PUT update, DELETE remove
- ✅ Status codes tell the truth: 200, 201, 204 for success; 400 and 404 for problems
- ✅ Minimal APIs for small services; controllers for larger, attribute-driven ones
- ✅ Model binding fills your parameters; DTOs control what you accept and return
- ✅ Version from day one and never return entities directly or skip validation
- ✅ Next lesson: Middleware & Filters — cross-cutting concerns like logging and auth
Practice quiz
Which HTTP verb is used to CREATE a new resource?
- GET
- PUT
- POST
- DELETE
Answer: POST. POST creates a new resource; GET reads, PUT updates/replaces, DELETE removes.
What status code should a successful POST that creates a resource return?
- 201 Created
- 200 OK
- 204 No Content
- 404 Not Found
Answer: 201 Created. A successful create returns 201 Created, ideally with a Location header to the new resource.
What status code is appropriate for a successful DELETE with nothing to send back?
- 200 OK
- 201 Created
- 400 Bad Request
- 204 No Content
Answer: 204 No Content. 204 No Content signals success with no body, the typical response for a DELETE.
When should an API return 404 Not Found?
- When input validation fails
- When the requested resource id doesn't exist
- When the server crashes
- On every successful GET
Answer: When the requested resource id doesn't exist. 404 means the request was fine but the resource doesn't exist; invalid input is 400 instead.
What does 'stateless' mean for a REST API?
- The server keeps no memory of the client between requests
- The server stores no data
- Requests must use GET only
- Responses are never cached
Answer: The server keeps no memory of the client between requests. Each request carries everything the server needs, so any server behind a load balancer can handle it.
Why should an API return a DTO instead of the database entity?
- DTOs are faster to serialise
- Entities can't be serialised
- To control the public shape and avoid leaking internal fields
- DTOs are required by HTTP
Answer: To control the public shape and avoid leaking internal fields. A DTO is the public contract; returning the entity ties the API to storage and can leak sensitive fields.
What is model binding in ASP.NET Core?
- Mapping database tables to classes
- Reading values from the incoming request into typed method parameters
- Validating the database schema
- Caching responses
Answer: Reading values from the incoming request into typed method parameters. Model binding pulls route, query, and body values out of the request and hands them to your method as typed parameters.
What is the difference between PUT and POST?
- They are identical
- PUT creates and POST deletes
- POST is read-only
- POST creates a new resource; PUT replaces an existing one at a known URL
Answer: POST creates a new resource; PUT replaces an existing one at a known URL. POST creates (server assigns the id, 201); PUT replaces an existing resource at a known URL (200 or 404).
What is 'over-posting' (mass assignment) and how do you prevent it?
- Sending too many requests; add rate limiting
- Binding the request straight onto your entity, letting clients set fields they shouldn't; bind to a narrow input DTO
- Posting to the wrong URL; fix the route
- Returning too much data; paginate
Answer: Binding the request straight onto your entity, letting clients set fields they shouldn't; bind to a narrow input DTO. Binding directly to the entity lets a client set fields like Id or IsAdmin; bind to a narrow input DTO instead.
A GET request, by REST convention, must never do what?
- Return JSON
- Use a status code
- Change data on the server
- Have a URL
Answer: Change data on the server. GET must be safe and read-only; caches and crawlers assume a GET never changes state.
Continue this course
- Previous: JSON Processing
- Next: Middleware & Filters