By the end of this session, you will be able to:
| Time | Segment | Type | Duration |
|---|---|---|---|
| 0:00 | HTTP methods — the full picture | Theory | 30 min |
| 0:30 | Idempotency & safety explained | Theory | 15 min |
| 0:45 | HTTP status codes — by family | Theory | 25 min |
| 1:10 | Live demo — wrong codes in the wild | Demo | 15 min |
| 1:25 | Lab — fix the broken API | Lab | 25 min |
| 1:50 | Wrap-up | Discussion | 10 min |
| Method | Use case | Request body? | Idempotent? | Safe? |
|---|---|---|---|---|
| GET | Retrieve a resource | No | Yes | Yes |
| POST | Create a new resource | Yes | No | No |
| PUT | Replace a resource entirely | Yes | Yes | No |
| PATCH | Partially update a resource | Yes | No | No |
| DELETE | Remove a resource | No | Yes | No |
Ask yourself what the operation is doing, not what it sounds like.
POST for everything because it is easy.
POST /tasks/complete/5 should be PATCH /tasks/5 with a body.
GET to pass filters is non-standard and breaks many HTTP clients and proxies. Use query parameters instead: GET /tasks?status=open.
PUT requires the full object. If you only send the fields you want to change, the missing fields get set to null or default. Use PATCH for partial updates.
PUT handler creates the resource when it does not exist, make sure it does not create a second copy on retry.
GET, HEAD, and OPTIONS are safe.
GET, PUT, and DELETE are idempotent.
DELETE is idempotent (second call still returns success) but it is not safe — the first call changed the server state.
| Method | Safe | Idempotent |
|---|---|---|
| GET | Yes | Yes |
| POST | No | No |
| PUT | No | Yes |
| PATCH | No | No |
| DELETE | No | Yes |
POST that creates a new record each time will produce duplicate data with no error.PUT simply sets the same value again — harmless.| Code | Name | When to return it |
|---|---|---|
200 |
OK | The default success response — use for GET, PUT, and PATCH when returning a body |
201 |
Created | A POST that created a new resource. Include a Location header pointing to the new resource URL |
204 |
No Content | Success with no response body — use for DELETE, or PATCH when nothing is returned |
Ok(resource) for 200, Created(location, resource) for 201, NoContent() for 204.
| Code | Name | When to return it |
|---|---|---|
400 |
Bad Request | Malformed request body, missing required fields, failed model validation |
401 |
Unauthorized | No token provided, or the token is invalid / expired |
403 |
Forbidden | Valid token, but the user does not have permission for this action |
404 |
Not Found | The requested resource does not exist — wrong ID, deleted record |
409 |
Conflict | State conflict — duplicate email on registration, concurrent edit collision |
422 |
Unprocessable Entity | Request is well-formed but fails a business rule (e.g. closing a task that is already closed) |
429 |
Too Many Requests | Rate limit exceeded — covered in Session 16 |
3xx — Redirection
| Code | Name | When to use |
|---|---|---|
301 |
Moved Permanently | A URL has changed permanently — tell clients to update their bookmarks |
304 |
Not Modified | Response has not changed since the client's cached version (ETag / conditional requests) |
5xx — Server Error
| Code | Name | When to use |
|---|---|---|
500 |
Internal Server Error | Unhandled exception — let the middleware produce this; never return it manually |
503 |
Service Unavailable | Overloaded or down — typically produced by the load balancer, not your code |
Real APIs get this wrong more often than you would expect. Here is what to look for in Postman:
200 OK but the JSON reads {"error": "user not found"}.
Frontend error handlers never fire; the error is silently ignored.
{"success": false, "message": "wrong password"} with status 200.
Monitoring tools report 100% uptime even during an outage.
Location header to follow; the UI must do an extra GET to find the new resource's ID.
Find and fix these five bugs in the provided controller:
200 instead of 201
200 with a body instead of 204
null instead of 404
500 manually — should be 400
// Bug 1: GET that modifies data
[HttpGet("complete/{id}")]
public IActionResult MarkComplete(int id)
{
var task = _tasks.First(t => t.Id == id);
task.IsComplete = true; // side effect!
return Ok(task);
}
// Bug 2: POST returning 200
[HttpPost]
public IActionResult Create(TaskItem task)
{
_tasks.Add(task);
return Ok(task); // should be Created(...)
}
// Bug 3: DELETE returning body
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
var task = _tasks.First(t => t.Id == id);
_tasks.Remove(task);
return Ok(task); // should be NoContent()
}
// Bug 4: Missing 404
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
var task = _tasks.FirstOrDefault(t => t.Id == id);
return Ok(task); // null if not found!
}
// Bug 5: Manual 500 for validation
[HttpPut("{id}")]
public IActionResult Update(int id, TaskItem task)
{
if (string.IsNullOrEmpty(task.Title))
return StatusCode(500, "Title required"); // should be 400
return Ok(task);
}
Location header pointing to the created resourceSession 05 — Error Handling & Clean Error Responses
ValidationProblemDetails
Build a ProductsController with in-memory storage and full HTTP method + status code compliance.
/api/products returns 200 with the full list/api/products/{id} returns 200 with the product or 404 if not found/api/products creates the product and returns 201 with the new resource in the body/api/products/{id} replaces the full product and returns 200, or 404 if it does not exist/api/products/{id} removes the product and returns 204 with no bodyAcceptance criteria:
200 when an error occurredName field. Return 400 if Name is empty or missing from the request body.