Adding a POST Insert Endpoint with Validation in .NET Aspire on Linux
Reading data from an API is only half the story. Eventually every application needs to accept new records, and that means building a POST endpoint that receives a request body, validates the input, persists it to the database, and returns a meaningful status code. Skipping the validation step is tempting during prototyping, but production APIs that accept unvalidated input become a source of corrupted data that is harder to clean up than to prevent.
In his video "Adding a POST Insert Endpoint with Validation in .NET Aspire on Linux," Tim Corey adds an insert endpoint to the Tiny Ticket API, creates a dedicated input record type, wires up .NET's built-in validation pipeline for minimal APIs, and configures Swagger to launch automatically when the API starts. The episode covers the full cycle from stored procedure to tested endpoint, including the validation error response format that .NET returns out of the box. If you are following the C# on Linux series or adding write operations to a minimal API for the first time, this article walks through each step.
Creating the Insert Record Type
[1:46 - 4:43] Before building the endpoint, Tim creates a data transfer object that represents the shape of an insert request. The existing TicketModel includes fields like Id and CreatedDate that the database generates automatically. Accepting those in a POST body would either be ignored or cause conflicts, so a separate type scopes the input to only the fields the caller should provide.
public record TicketInsertRecord(string Title, string Description, int Priority);public record TicketInsertRecord(string Title, string Description, int Priority);Using a record instead of a class is a deliberate choice. Records provide value-based equality and immutability by default, which fits the semantics of a request payload: the data arrives, gets validated, gets passed to the database, and is never modified in between. The three properties (Title, Description, Priority) map directly to the parameters of the spTickets_Insert stored procedure.
Mapping the POST Endpoint
[4:43 - 9:51] With the record type defined, the endpoint registration follows the same pattern as the GET routes but uses MapPost and binds the request body to the insert record:
app.MapPost("/api/tickets", async (TicketInsertRecord ticket, IDbConnection db) =>
{
await db.SaveDataAsync("spTickets_Insert", ticket);
return Results.NoContent();
});app.MapPost("/api/tickets", async (TicketInsertRecord ticket, IDbConnection db) =>
{
await db.SaveDataAsync("spTickets_Insert", ticket);
return Results.NoContent();
});Notice the route is /api/tickets without an ID segment, matching REST conventions where POST to a collection URL creates a new resource. The handler calls the stored procedure with the entire ticket object as the parameter bag. Dapper maps the record's properties to the SQL parameters by name.
Returning Results.NoContent() sends a 204 status code. Tim explains the reasoning: the insert succeeded, but there is nothing meaningful to return in the response body. Some APIs return the newly created object with a 201 Created status and a Location header pointing to the new resource, which is a valid alternative. For the Tiny Ticket project, 204 keeps things lean.
Testing the Insert Through Swagger
[9:51 - 14:43] Tim launches the project and navigates to Swagger. The POST endpoint appears with a request body schema matching the TicketInsertRecord properties. He fills in a test ticket with a title, description, and priority, then executes the request.
A 204 comes back, confirming the insert succeeded. To verify the data actually persisted, he switches to the GET all endpoint and executes it. The new ticket appears in the list alongside the original test records.
What the test also reveals is the gap without validation: sending an empty title, a missing description, or a priority of 99 all succeed with a 204. The database accepts whatever the API sends. That gap motivates the next section.
Adding Built-In Validation
[14:43 - 18:28] Starting with .NET 10, minimal APIs support a built-in validation pipeline that reads data annotation attributes from the input type and rejects invalid requests before the handler executes. Tim wires it up in two steps.
First, register the validation services in Program.cs. This single line activates the entire pipeline:
builder.Services.AddValidation();builder.Services.AddValidation();With the service registered, the framework inspects every request body for validation attributes before the handler runs. The second step is annotating the insert record with the rules each field must satisfy:
public record TicketInsertRecord(
[Required, MinLength(1)] string Title,
[Required] string Description,
[Range(1, 5)] int Priority
);public record TicketInsertRecord(
[Required, MinLength(1)] string Title,
[Required] string Description,
[Range(1, 5)] int Priority
);[Required] ensures the field is present and not null. [MinLength(1)] prevents empty strings from passing the required check (since an empty string is technically not null). [Range(1, 5)] constrains priority to a valid tier. These attributes are the same System.ComponentModel.DataAnnotations types that ASP.NET MVC controllers have used for years, but they now work in minimal APIs without additional middleware.
After saving and restarting, Tim sends a request with an empty title and a priority of 10. The response comes back as a 400 Bad Request with a structured error body:
{
"errors": {
"Title": ["The Title field is required."],
"Priority": ["The field Priority must be between 1 and 5."]
}
}The validation pipeline short-circuits the request before the handler runs, so no invalid data reaches the database. The error response follows the RFC 7807 Problem Details format, which API consumers can parse programmatically.
Auto-Launching Swagger on Startup
[19:44 - 20:31] A small quality-of-life improvement closes the episode. Every time Tim launched the API, he had to manually type /swagger into the browser URL. To automate that, he opens the API project's Properties/launchSettings.json and adds a launchUrl property to the HTTPS profile:
{
"profiles": {
"https": {
"launchUrl": "swagger"
}
}
}On the next launch, the browser opens directly to the Swagger UI instead of the default page. This saves a few seconds per debug cycle, which compounds across a full development session.
Conclusion
[20:09 - 20:31] Adding a POST endpoint to a minimal API involves creating a dedicated input record, mapping it to MapPost with the collection URL, and calling the stored procedure with the record as the parameter object. Validation in .NET 10 requires one service registration and standard data annotation attributes on the record properties. The framework handles the 400 response formatting automatically.
Series navigation: This article is part of the C# on Linux series building the Tiny Ticket app. Previous: Adding a Get By ID Endpoint. Next: Adding a PUT Update Endpoint.
Example Tip: When your insert stored procedure returns the new record's ID, change the return from Results.NoContent() to Results.Created($"/api/tickets/{newId}", result) to give callers a 201 status with a location header they can follow to fetch the created resource.
Watch full video on his YouTube Channel and gain more insights on building write endpoints in the C# on Linux series.

