| title | Tutorial: Create a Minimal API with ASP.NET Core |
|---|---|
| author | wadepickett |
| description | Learn how to build a minimal API with ASP.NET Core. |
| ai-usage | ai-assisted |
| ms.author | wpickett |
| ms.date | 06/20/2026 |
| monikerRange | >= aspnetcore-6.0 |
| uid | tutorials/min-web-api |
By Wade Pickett and Tom Dykstra
:::moniker range=">= aspnetcore-10.0"
Minimal APIs are architected to create HTTP APIs with minimal dependencies. They're ideal for microservices and apps that want to include only the minimum files, features, and dependencies in ASP.NET Core.
This tutorial teaches the basics of building a Minimal API with ASP.NET Core. Another approach to creating APIs in ASP.NET Core is to use controllers. For help with choosing between Minimal APIs and controller-based APIs, see xref:fundamentals/apis. For a tutorial on creating an API project based on controllers that contains more features, see Create a web API.
This tutorial creates the following API:
| API | Description | Request body | Response body |
|---|---|---|---|
GET /todoitems |
Get all to-do items | None | Array of to-do items |
GET /todoitems/complete |
Get completed to-do items | None | Array of to-do items |
GET /todoitems/{id} |
Get an item by ID | None | To-do item |
POST /todoitems |
Add a new item | To-do item | To-do item |
PUT /todoitems/{id} |
Update an existing item | To-do item | None |
PATCH /todoitems/{id} |
Partially update an item | Partial to-do item | None |
DELETE /todoitems/{id} |
Delete an item | None | None |
-
Start Visual Studio 2026 and select Create a new project.
-
In the Create a new project dialog:
- Select the ASP.NET Core Web API project type, and select Next.
- Name the project TodoApi, and select Next.
-
In the Additional information dialog:
- Confirm the Framework is .NET 10.0 (Long Term Support).
- Confirm the checkbox for Enable OpenAPI support is checked.
- Confirm the checkbox for Use controllers is not checked. Unchecking this setting creates a Minimal API project as required for this tutorial rather than a controller-based one.
- Select Create.
-
Open the integrated terminal.
-
Change directories (
cd) to the folder that will contain the project folder. -
Run the following commands:
dotnet new webapi -o TodoApi cd TodoApi code -r ../TodoApi -
When a dialog box asks if you want to trust the authors, select Yes.
-
When a dialog box asks if you want to add required assets to the project, select Yes.
The preceding commands create a new web Minimal API project and open it in Visual Studio Code.
The Program.cs file generated by the template contains the following code:
:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/todo/Program.cs?name=snippet_templatestart":::
The preceding code:
- Creates a xref:Microsoft.AspNetCore.Builder.WebApplicationBuilder and a xref:Microsoft.AspNetCore.Builder.WebApplication with preconfigured defaults.
- Registers the in-box OpenAPI document generator with
builder.Services.AddOpenApi(). - Maps the generated OpenAPI document at
/openapi/v1.jsonwithapp.MapOpenApi(), in the Development environment only. - Defines a sample
GET /weatherforecastendpoint that returns five randomly generatedWeatherForecastrecords.
In this tutorial, you'll replace the WeatherForecast with a new Todo sample, with endpoints to create, read, update, and delete items, backed by a model and a database.
NuGet packages must be added to support the database and diagnostics used in this tutorial.
- From the Tools menu, select NuGet Package Manager > Manage NuGet Packages for Solution.
- Select the Browse tab.
- Enter Microsoft.EntityFrameworkCore.InMemory in the search box, and then select
Microsoft.EntityFrameworkCore.InMemory. - Select the Project checkbox in the right pane and then select Install.
- Follow the preceding instructions to add the
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCorepackage.
-
Run the following commands:
dotnet add package Microsoft.EntityFrameworkCore.InMemory dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
- In the project folder, create a file named
Todo.cswith the following code:
:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/todoGroup/Todo.cs":::
The preceding code creates the model for this app. A model is a class that represents data that the app manages.
- Create a file named
TodoDb.cswith the following code:
:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/todo/TodoDb.cs":::
The preceding code defines the database context, which is the main class that coordinates Entity Framework functionality for a data model. This class derives from the xref:Microsoft.EntityFrameworkCore.DbContext?displayProperty=fullName class.
The webapi template added a sample GET /weatherforecast endpoint to Program.cs and a WeatherForecast record at the bottom of the file. The sample is just a placeholder to show the template works — replace both with the Todo endpoints below.
- In
Program.cs, replace theWeatherForecastsample endpoint and theWeatherForecastrecord with the following code:
:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/todo/Program.cs?name=snippet_minimal_start_all":::
The OpenAPI lines added by the template (builder.Services.AddOpenApi() and app.MapOpenApi()) remain in place. They now describe the Todo endpoints instead of the sample weather endpoint.
The following highlighted code adds the database context to the dependency injection (DI) container and enables displaying database-related exceptions:
The DI container provides access to the database context and other services.
This tutorial uses Endpoints Explorer and .http files to test the API.
The template already generates the OpenAPI document at /openapi/v1.json. To explore and test the API from a browser, add a UI that consumes that document. This tutorial uses NSwag.
-
Run the following command:
dotnet add package NSwag.AspNetCore
For more information on using OpenAPI and NSwag with ASP.NET, see xref:tutorials/web-api-help-pages-using-swagger.
The template already generates the OpenAPI document at /openapi/v1.json. To explore and test the API from a browser, add a UI that consumes that document. This tutorial uses NSwag.
Add the NSwag.AspNetCore package, which provides the middleware that serves the UI:
-
Run the following command:
dotnet add package NSwag.AspNetCore
In this tutorial, NSwag is used only for its Swagger UI middleware, which reads the OpenAPI document already generated by OpenAPI.
-
In
Program.cs, add the following highlighted code inside theif (app.Environment.IsDevelopment())block, right after theapp.MapOpenApi();line:The preceding code enables the NSwag UI in Development only and points it at
/openapi/v1.json, the document produced byMapOpenApi(). The UI itself is served at/swagger.[!NOTE] NSwag is one of several options for an OpenAPI UI. Alternatives such as Scalar or
Swashbuckle.AspNetCore.SwaggerUiconsume the same OpenAPI document and can be substituted for NSwag without changing the rest of the tutorial.
The following code in Program.cs creates an HTTP POST endpoint /todoitems that adds data to the in-memory database:
Run the app. The browser displays a 404 error because there's no longer a / endpoint.
The POST endpoint will be used to add data to the app.
-
Press Ctrl+F5 to run the app without debugging. Visual Studio launches the Kestrel web server and trusts the development certificate if needed.
-
Select View > Other Windows > Endpoints Explorer.
-
Right-click the POST endpoint and select Generate request.
A new file is created in the project folder named
TodoApi.http, with contents similar to the following example:@TodoApi_HostAddress = https://localhost:7031 POST {{TodoApi_HostAddress}}/todoitems ###
- The first line creates a variable that is used for all of the endpoints.
- The next line defines a POST request.
- The triple hashtag (
###) line is a request delimiter: what comes after it is for a different request.
-
The POST request needs headers and a body. To define those parts of the request, add the following lines immediately after the POST request line:
Content-Type: application/json { "name":"walk dog", "isComplete":true }The preceding code adds a Content-Type header and a JSON request body. The TodoApi.http file should now look like the following example, but with your port number:
@TodoApi_HostAddress = https://localhost:7057 POST {{TodoApi_HostAddress}}/todoitems Content-Type: application/json { "name":"walk dog", "isComplete":true } ###
-
Select the Send request link that is above the
POSTrequest line.The POST request is sent to the app and the response is displayed in the Response pane.
-
With the app still running, in the browser, navigate to
https://localhost:{port}/swaggerto display the API testing page generated by Swagger. -
On the Swagger API testing page, select Post /todoitems > Try it out.
-
Note that the Request body field contains a generated example format reflecting the parameters for the API.
-
In the request body enter JSON for a to-do item, without specifying the optional
id:{ "name":"walk dog", "isComplete":true } -
Select Execute.
Swagger provides a Responses pane below the Execute button.
Note a few of the useful details:
- cURL: Swagger provides an example cURL command in Unix/Linux syntax, which can be run at the command line with any bash shell that uses Unix/Linux syntax, including Git Bash from Git for Windows.
- Request URL: A simplified representation of the HTTP request made by Swagger UI's JavaScript code for the API call. Actual requests can include details such as headers and query parameters and a request body.
- Server response: Includes the response body and headers. The response body shows the
idwas set to1. - Response Code: A 201
HTTPstatus code was returned, indicating that the request was successfully processed and resulted in the creation of a new resource.
Your Program.cs includes a single PUT endpoint, defined with MapPut:
| API | Description | Request body | Response body |
|---|---|---|---|
GET /todoitems |
Get all to-do items | None | Array of to-do items |
GET /todoitems/complete |
Get all completed to-do items | None | Array of to-do items |
GET /todoitems/{id} |
Get an item by ID | None | To-do item |
Test the app by calling the GET endpoints from a browser or by using Endpoints Explorer. The following steps are for Endpoints Explorer.
-
In Endpoints Explorer, right-click the first GET endpoint, and select Generate request.
The following content is added to the
TodoApi.httpfile:GET {{TodoApi_HostAddress}}/todoitems ###
-
Select the Send request link that is above the new
GETrequest line.The GET request is sent to the app and the response is displayed in the Response pane.
-
The response body is similar to the following JSON:
[ { "id": 1, "name": "walk dog", "isComplete": true } ] -
In Endpoints Explorer, right-click the
/todoitems/{id}GET endpoint and select Generate request. The following content is added to theTodoApi.httpfile:GET {{TodoApi_HostAddress}}/todoitems/{id} ###
-
Replace
{id}with1. -
Select the Send request link that is above the new GET request line.
The GET request is sent to the app and the response is displayed in the Response pane.
-
The response body is similar to the following JSON:
{ "id": 1, "name": "walk dog", "isComplete": true }
Test the app by calling the endpoints from a browser or Swagger.
-
In Swagger select GET /todoitems > Try it out > Execute.
-
Alternatively, call GET /todoitems from a browser by entering the URI
http://localhost:{port}/todoitems. For example,http://localhost:7032/todoitems
The call to GET /todoitems produces a response similar to the following:
[
{
"id": 1,
"name": "walk dog",
"isComplete": true
}
]-
Call GET /todoitems/{id} in Swagger to return data from a specific id:
- Select GET /todoitems > Try it out.
- Set the id field to
1and select Execute.
-
Alternatively, call GET /todoitems from a browser by entering the URI
http://localhost:{port}/todoitems/1. For example,http://localhost:7032/todoitems/1 -
The response is similar to the following:
{ "id": 1, "name": "walk dog", "isComplete": true }
This app uses an in-memory database. If the app is restarted, the GET request doesn't return any data. If no data is returned, POST data to the app and try the GET request again.
ASP.NET Core automatically serializes the object to JSON and writes the JSON into the body of the response message. The response code for this return type is 200 OK, assuming there are no unhandled exceptions. Unhandled exceptions are translated into 5xx errors.
The return types can represent a wide range of HTTP status codes. For example, GET /todoitems/{id} can return two different status values:
- If no item matches the requested ID, the method returns a 404 status xref:Microsoft.AspNetCore.Mvc.ControllerBase.NotFound%2A error code.
- Otherwise, the method returns 200 with a JSON response body. Returning
itemresults in an HTTP 200 response.
The sample app implements a single PUT endpoint using MapPut:
This method is similar to the MapPost method, except it uses HTTP PUT. A successful response returns 204 (No Content). According to the HTTP specification, a PUT request requires the client to send the entire updated entity, not just the changes. To support partial updates, use HTTP PATCH.
This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PUT call. Call GET to ensure there's an item in the database before making a PUT call.
Update the Todo item that has Id = 1 and set its name to "feed fish".
-
In Endpoints Explorer, right-click the PUT endpoint, and select Generate request.
The following content is added to the
TodoApi.httpfile:PUT {{TodoApi_HostAddress}}/todoitems/{id} ###
-
In the PUT request line, replace
{id}with1. -
Add the following lines immediately after the PUT request line:
Content-Type: application/json { "id": 1, "name": "feed fish", "isComplete": false }
The preceding code adds a Content-Type header and a JSON request body.
-
Select the Send request link that is above the new PUT request line.
The PUT request is sent to the app and the response is displayed in the Response pane. The response body is empty, and the status code is 204.
Use Swagger to send a PUT request:
-
Select Put /todoitems/{id} > Try it out.
-
Set the id field to
1. -
Set the request body to the following JSON:
{ "id": 1, "name": "feed fish", "isComplete": false } -
Select Execute.
A PATCH endpoint lets clients send only the fields they want to update, such as renaming a Todo item without resending its completion status. This differs from a PUT request which replaces the entire item, so the client must send every field even to change just one.
The PATCH endpoint uses a TodoPatchDto class with nullable properties to properly handle partial updates. Using nullable properties allows the endpoint to distinguish between a field that wasn't provided (null) versus a field explicitly set to a value (including false for boolean fields). Without nullable properties, a non-nullable bool would default to false, potentially overwriting an existing true value when that field wasn't included in the request.
Create a file named TodoPatchDto.cs with the following code:
:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/todo/TodoPatchDto.cs":::
The TodoPatchDto class uses nullable properties (string? and bool?) to distinguish between a field that wasn't provided in the request versus a field explicitly set to a value.
The sample app implements a single PATCH endpoint using MapPatch:
This method is similar to the MapPut method, except it uses HTTP PATCH and only updates the fields provided in the request. A successful response returns 204 (No Content).
Note
PATCH operations allow partial updates to resources. For more advanced partial updates using JSON Patch documents, see xref:web-api/jsonpatch.
This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PATCH call. Call GET to ensure there's an item in the database before making a PATCH call.
Update only the name property of the Todo item that has Id = 1 and set its name to "run errands".
-
In Endpoints Explorer, right-click the PATCH endpoint, and select Generate request.
The following content is added to the
TodoApi.httpfile:PATCH {{TodoApi_HostAddress}}/todoitems/{id} ###
-
In the PATCH request line, replace
{id}with1. -
Add the following lines immediately after the PATCH request line:
Content-Type: application/json { "name": "run errands" }
The preceding code adds a Content-Type header and a JSON request body with only the field to update.
-
Select the Send request link that is above the new PATCH request line.
The PATCH request is sent to the app and the response is displayed in the Response pane. The response body is empty, and the status code is 204.
Use Swagger to send a PATCH request:
-
Select Patch /todoitems/{id} > Try it out.
-
Set the id field to
1. -
Set the request body to the following JSON:
{ "name": "run errands" } -
Select Execute.
Your Program.cs includes a single DELETE endpoint, defined with MapDelete:
-
In Endpoints Explorer, right-click the DELETE endpoint and select Generate request.
A DELETE request is added to
TodoApi.http. -
Replace
{id}in the DELETE request line with1. The DELETE request should look like the following example:DELETE {{TodoApi_HostAddress}}/todoitems/1 ###
-
Select the Send request link for the DELETE request.
The DELETE request is sent to the app and the response is displayed in the Response pane. The response body is empty, and the status code is 204.
Use Swagger to send a DELETE request:
-
Select DELETE /todoitems/{id} > Try it out.
-
Set the ID field to
1and select Execute.The DELETE request is sent to the app and the response is displayed in the Responses pane. The response body is empty, and the Server response status code is 204.
The Program.cs you wrote repeats the todoitems URL prefix each time it sets up an endpoint. APIs often have groups of endpoints with a common URL prefix, and the xref:Microsoft.AspNetCore.Builder.EndpointRouteBuilderExtensions.MapGroup%2A method is available to help organize such groups. It reduces repetitive code and allows for customizing entire groups of endpoints with a single call to methods like xref:Microsoft.AspNetCore.Builder.AuthorizationEndpointConventionBuilderExtensions.RequireAuthorization%2A and xref:Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions.WithMetadata%2A.
Replace the contents of Program.cs with the following code:
:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/todoGroup/Program.cs" id="snippet_all":::
:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/todoGroup_SwaggerVersion/Program.cs" id="snippet_all":::
The preceding code has the following changes:
- Adds
var todoItems = app.MapGroup("/todoitems");to set up the group using the URL prefix/todoitems. - Changes all the
app.Map<HttpVerb>methods totodoItems.Map<HttpVerb>. - Removes the URL prefix
/todoitemsfrom theMap<HttpVerb>method calls.
Test the endpoints to verify that they work the same.
Returning xref:Microsoft.AspNetCore.Http.TypedResults rather than xref:Microsoft.AspNetCore.Http.Results has several advantages, including testability and automatically returning the response type metadata for OpenAPI to describe the endpoint. For more information, see TypedResults vs Results.
The Map<HttpVerb> methods can call route handler methods instead of using lambdas. To see an example, update Program.cs with the following code:
:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/todoTypedResults/Program.cs" id="snippet_all":::
:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/todoTypedResults_SwaggerVersion/Program.cs" id="snippet_all":::
The Map<HttpVerb> code now calls methods instead of lambdas:
:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/todoTypedResults/Program.cs" id="snippet_group":::
These methods return objects that implement xref:Microsoft.AspNetCore.Http.IResult and are defined by xref:Microsoft.AspNetCore.Http.TypedResults:
:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/todoTypedResults/Program.cs" id="snippet_handlers":::
Unit tests can call these methods and test that they return the correct type. For example, if the method is GetAllTodos:
:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/todoTypedResults/Program.cs" id="snippet_getalltodos":::
Unit test code can verify that an object of type Ok<Todo[]> is returned from the handler method. For example:
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
// Arrange
var db = CreateDbContext();
// Act
var result = await TodosApi.GetAllTodos(db);
// Assert: Check for the correct returned type
Assert.IsType<Ok<Todo[]>>(result);
}Currently your API exposes the entire Todo object. In production applications, a subset of the model is often used to restrict the data that can be input and returned. There are multiple reasons behind this and security is a major one. The subset of a model is usually referred to as a Data Transfer Object (DTO), input model, or view model. DTO is used in this article.
A DTO can be used to:
- Prevent over-posting.
- Hide properties that clients aren't supposed to view.
- Omit some properties to reduce payload size.
- Flatten object graphs that contain nested objects. Flattened object graphs can be more convenient for clients.
To demonstrate the DTO approach, update the Todo class to include a secret field:
:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/todoDTO/Todo.cs":::
The secret field needs to be hidden from this app, but an administrative app could choose to expose it.
Verify you can post and get the secret field.
Create a file named TodoItemDTO.cs with the following code:
:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/todoDTO/TodoItemDTO.cs":::
Replace the contents of the Program.cs file with the following code to use this DTO model:
:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/todoDTO/Program.cs" id="snippet_all":::
:::code language="csharp" source="~/tutorials/min-web-api/samples/10.x/todoDTO_SwaggerVersion/Program.cs" id="snippet_all":::
Verify you can post and get all fields except the secret field.
If you run into a problem you can't resolve, compare your code to the completed project. View or download completed project (how to download).
- Configure JSON serialization options.
- Handle errors and exceptions: The developer exception page is enabled by default in the
Developmentenvironment for Minimal API apps. For information about how to handle errors and exceptions, see Handle errors in ASP.NET Core APIs. - For an example of testing a Minimal API app, see this GitHub sample.
- OpenAPI support in Minimal APIs.
- Quickstart: Publish to Azure.
- Organizing ASP.NET Core Minimal APIs.
See xref:fundamentals/minimal-apis
:::moniker-end






