What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more
To implement CRUD in an ASP.NET Core API, define a clear HTTP contract, return responses that match each operation, validate input, and keep client-writable fields separate from persistence entities. Microsoft recommends Minimal APIs for new projects; controllers remain supported. This walkthrough uses Minimal APIs and a TodoItem resource to show how to create, read, update, and delete data without hiding important behavior behind vague success responses.
Start with the resource and its HTTP contract
Choose routes and response behavior before writing handlers. For a todo resource, a consistent route family is /api/todo-items for the collection and /api/todo-items/{id} for an individual item.
| Operation | Request | Successful outcome in this example | Other important outcome |
|---|---|---|---|
| Create | POST /api/todo-items |
201 Created with a Location header for the new item |
Validation error response for invalid input |
| Read collection | GET /api/todo-items |
200 OK with a JSON collection |
— |
| Read item | GET /api/todo-items/{id} |
200 OK with the matching item |
404 Not Found when no item matches |
| Replace item | PUT /api/todo-items/{id} |
204 No Content in this example |
404 Not Found when no item matches |
| Delete item | DELETE /api/todo-items/{id} |
204 No Content in this example |
404 Not Found when no item matches |
The status codes above describe this API’s chosen contract, not a claim that every API must use the same successful response for every operation. Microsoft documents Minimal APIs as a simplified option for new projects and continues to document controller-based APIs. Choose based on your team’s conventions, desired handler structure, and whether you are extending an existing application; the available documentation does not establish a quantitative performance comparison. See Microsoft’s ASP.NET Core APIs overview and its ASP.NET Core 10.0 Web API guidance.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Use input and output models instead of exposing the entity
A persistence entity may contain fields that clients should neither set nor see: for example, internal status, audit metadata, or server-managed identifiers. Binding a request directly to that broad entity can let a client submit fields the endpoint was not meant to accept. Separate request and response models make the boundary explicit.
#1 Best Overall
public sealed record CreateTodoRequest(string Title);
public sealed record UpdateTodoRequest(string Title, bool IsComplete);
public sealed record TodoResponse(int Id, string Title, bool IsComplete);
These are illustrative shapes: the create request omits an identifier because the API assigns it, while the update request contains the full representation this example expects for replacement. Adapt the fields and validation rules to the resource you actually expose. Microsoft identifies limiting writable and visible properties, preventing over-posting, reducing payload size, and flattening nested object graphs as reasons to use a DTO or input/view model. See the ASP.NET Core 10.0 controller tutorial.
Make create identify the new resource
A weak create handler returns a generic success but gives the caller no clear URI for the resource it just created. A useful creation response communicates that the resource now exists and points to it. Microsoft’s controller tutorial demonstrates CreatedAtAction, which returns 201 Created and a Location header for the created resource. The client can use that URI to retrieve the item.
app.MapPost("/api/todo-items", (CreateTodoRequest request) =>
{
if (string.IsNullOrWhiteSpace(request.Title))
return Results.ValidationProblem(new Dictionary<string, string[]>
{
["title"] = ["A title is required."]
});
var item = new TodoResponse(1, request.Title.Trim(), false);
return Results.Created($"/api/todo-items/{item.Id}", item);
});
The identifier assignment and storage in this small snippet are illustrative, not a production persistence implementation. In a real handler, obtain the saved item’s identifier from your persistence layer and build the location from that identifier. The controller equivalent can use CreatedAtAction when the action route identifies the new item.
Rank #2
Keep collection and item reads distinct
A collection read and a single-item read answer different questions. Return the collection as JSON for GET /api/todo-items. For GET /api/todo-items/{id}, return the matching representation when it exists and a not-found result when it does not. Returning an empty object for an absent item makes absence look like a successful read and forces clients to infer what happened.
app.MapGet("/api/todo-items", () => Results.Ok(items));
app.MapGet("/api/todo-items/{id:int}", (int id) =>
items.TryGetValue(id, out var item)
? Results.Ok(item)
: Results.NotFound());
Here, items represents whatever repository or data-access component the application uses; it is not defined as a complete storage implementation. Microsoft’s Minimal API tutorial illustrates successful JSON reads and a not-found outcome. That tutorial is published at an ASP.NET Core 6.0 versioned URL, so use it as an example of response behavior rather than as a current framework setup guide: Minimal API tutorial.
Make PUT a full replacement, and define PATCH separately
In the documented example, PUT represents a full update: the client sends the entire updated entity, not just whichever fields it wants to change. A handler that accepts a sparse body while describing the operation as full replacement leaves clients unsure whether omitted values are preserved, cleared, or rejected.
app.MapPut("/api/todo-items/{id:int}", (int id, UpdateTodoRequest request) =>
{
if (string.IsNullOrWhiteSpace(request.Title))
return Results.ValidationProblem(new Dictionary<string, string[]>
{
["title"] = ["A title is required."]
});
if (!items.ContainsKey(id))
return Results.NotFound();
items[id] = new TodoResponse(id, request.Title.Trim(), request.IsComplete);
return Results.NoContent();
});
This example’s UpdateTodoRequest includes every client-managed field in the representation. The tutorial example returns 204 No Content after a successful PUT without a response body; an API could choose a different response contract if it documents it consistently.
Use PATCH when the operation is partial
If a client should change only selected fields, define a separate PATCH operation and specify exactly which fields it accepts and how omitted fields behave. Do not silently treat a partial body as a full replacement, and do not assume a particular patch-document format without defining it for clients. The cited Minimal API tutorial distinguishes PUT’s full-update example from partial updates handled with PATCH.
Choose and document a deletion outcome
Deletion needs an explicit contract too. This example returns 204 No Content when an item is removed and 404 Not Found when the identifier does not match an item. Those are the choices made for this API example, not a universal requirement for every DELETE endpoint.
Rank #4
app.MapDelete("/api/todo-items/{id:int}", (int id) =>
{
if (!items.Remove(id))
return Results.NotFound();
return Results.NoContent();
});
If your application instead needs to return a representation or report a queued deletion, make that behavior clear to clients and keep it consistent with the operation. The storage call here is illustrative; the cited sources do not establish a general DELETE response rule.
Validate input and keep errors machine-readable
Reject invalid input at the API boundary rather than letting malformed values become confusing downstream failures. Keep error responses structured so clients can inspect which fields failed and present useful feedback. The examples use Results.ValidationProblem for a validation response; adapt the rules and messages to the resource’s requirements.
Controller-based APIs offer a documented alternative: with [ApiController], invalid model state can trigger an automatic HTTP 400 response. ASP.NET Core guidance also documents ValidationProblemDetails for validation output and ProblemDetails for error status codes. That convention helps make error information machine-readable; it does not remove the need to define meaningful validation rules. See the ASP.NET Core 10.0 Web API guidance.
Best Value
Verify the contract with real requests
Use a request tool to check both the expected response and important failure cases. Microsoft’s controller tutorial lists .http files, http-repl, curl, and Fiddler among request-testing options. The requests below are examples to send against an API running at https://localhost:5001; change the host and port to match your local application.
POST /api/todo-items HTTP/1.1
Host: localhost:5001
Content-Type: application/json
{"title":"Review the API contract"}
Check that a successful create returns 201 Created, includes a Location header for the new item, and returns the created representation. Then request that location and confirm the item can be read.
GET /api/todo-items/1 HTTP/1.1
Host: localhost:5001
Also request an identifier that does not exist and confirm the API returns its documented not-found response rather than an empty success. For a full update, send every client-managed field:
PUT /api/todo-items/1 HTTP/1.1
Host: localhost:5001
Content-Type: application/json
{"title":"Review the updated API contract","isComplete":true}
Finally, send an invalid create request, such as one with a blank title, and inspect the structured error response. These are reproducible checks, not a claim that the snippets have been run. The tutorial’s listed tools and controller examples are available in Microsoft’s ASP.NET Core 10.0 controller tutorial.
Quick Recap
Bad patterns and their better replacements
| Fragile pattern | Why it confuses or exposes the API | Better direction |
|---|---|---|
| Choosing a framework style by habit and calling the alternative obsolete | It misrepresents current ASP.NET Core guidance and may disregard an existing codebase’s conventions. | Use Minimal APIs as Microsoft’s recommendation for new projects, or use supported controllers where their structure suits the project. |
| Returning generic success after create | The caller has no clear URI to use for the new resource. | Return a creation response with the resource location; the documented controller pattern uses CreatedAtAction for 201 and Location. |
| Returning an empty object when an item is missing | Clients cannot distinguish absence from a successful read of an empty representation. | Return the item when found and a clear not-found result otherwise. |
| Accepting sparse input as full PUT | Clients cannot know what happens to omitted fields. | Require the full representation for the replacement operation; define PATCH separately for partial changes. |
| Returning ad hoc validation errors | Client code must handle inconsistent error shapes. | Use structured validation and ProblemDetails conventions. |
| Binding a broad entity as the request model | Clients may be able to submit or observe fields outside the API contract. | Use DTOs or input/output models that restrict writable and visible fields. |
| Claiming an endpoint works without showing how to check it | Readers cannot reproduce the expected request or inspect failure behavior. | Provide sample requests and verify success, missing-resource, and invalid-input cases with a request tool. |
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

