How to Return 201 Created with a Location Header from a Minimal API
When a POST creates something, the right answer isn't a plain 200 OK. It's 201 Created, with a Location header that says where the new thing lives, and usually the new thing in the body. ASP.NET Core minimal APIs have two ways to do it. This guide shows both, with the real responses.
Prerequisites
- An ASP.NET Core minimal API. The examples use .NET 10 and a
categoriestable, saved with EF Core.
Step 1: Return Created with a Path
TypedResults.Created takes the address of the new resource and the response body. Build the address after saving, when the ID exists:
db.Categories.Add(category);
await db.SaveChangesAsync(cancellationToken);
var response = new CategoryResponse(category.Id, category.Name, category.CreatedAtUtc);
return TypedResults.Created($"/api/v1/categories/{category.Id}", response);
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: /api/v1/categories/1
{
"id": 1,
"name": "Cables",
"createdAtUtc": "2026-11-04T10:48:37.000261Z"
}

That's what HTTP asks for. RFC 9110 says: "The primary resource created by the request is identified by either a Location header field in the response or, if no Location header field is received, by the target URI." A client can read the header and go straight to the new row:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"id": 1,
"name": "Cables",
"createdAtUtc": "2026-11-04T10:48:37.000261Z"
}
Step 2: Build the Address from a Named Route
A hard-coded path can drift away from the real route. A named route avoids that: give the GET endpoint a name with WithName, and use CreatedAtRoute, which looks the address up:
d.MapGet("/get-named/{id:int}", (int id) => Results.Ok(new { id })).WithName("GetNamedCategory");
d.MapPost("/created-at-route", async (CategoryRequest request, InventoryDbContext db, TimeProvider clock) =>
{
var category = new Category { Name = request.Name, CreatedAtUtc = clock.GetUtcNow().UtcDateTime };
db.Categories.Add(category);
await db.SaveChangesAsync();
return TypedResults.CreatedAtRoute(new { category.Id, category.Name }, "GetNamedCategory", new { id = category.Id });
});
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: http://localhost:5190/demo/get-named/503
{
"id": 503,
"name": "Named route"
}
The Location is now the full address, with the host. The route handlers page explains the name's purpose: "Using a named endpoint avoids having to hard code paths in an app". The same page says "Endpoint names must be globally unique", and that they're used "as the OpenAPI operation identifier (ID) when OpenAPI support is enabled".
Which One to Use
Created("/path", body) | CreatedAtRoute(body, "Name", values) | |
|---|---|---|
Location | The path you wrote | The full address, from the route |
| If the route changes | You have to update the string | It follows the route |
| Setup | None | WithName on the GET |
This API uses the first: the path is short, and it sits a few lines from the route it points at. Both are valid. A relative Location is allowed too. RFC 9110's section on Location says: "When it has the form of a relative reference … the final value is computed by resolving it against the target URI".
Common Mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
Returning Ok after creating | Clients can't tell it was created, and have no address | Return Created |
| Building the address before saving | The ID is still 0 | Save first |
| Returning the entity as the body | It exposes every column, and can loop | Return a response record |
| Forgetting to declare the return type | CS1593 with mixed results in a lambda | Use a named method with Results<…> |
FAQ
Do I have to return the new object?
No. TypedResults.Created("/demo/1") sends only the status and the header, as this response from a temporary endpoint shows. Returning the object saves the client a second request.
HTTP/1.1 201 Created
Location: /demo/1
What about a POST that doesn't create anything?
Then 201 isn't right. Archiving a product here answers 204 No Content, because nothing new exists.
Conclusion
After saving, return TypedResults.Created($"/api/v1/categories/{id}", response). The client gets 201, a Location header and the new resource. Use CreatedAtRoute with WithName if you'd rather build the address from the route.
Additional Resources
- Responses in Minimal API apps — every built-in result.
- Named endpoints and link generation — route names.
- RFC 9110: 201 Created — what the status means.
