Skip to main content

ASP.NET Core REST API Tutorial 2026: Build a Production API

Learn how to build a production-ready ASP.NET Core REST API in 2026. Step-by-step C# tutorial with CRUD, EF Core, validation & JWT auth. Start now!

If you searched for an ASP.NET Core REST API tutorial, you want more than a "hello world" endpoint — you want to build a REST API in C# that you can actually ship. This 2026 guide walks you through building a production-ready ASP.NET Core Web API step by step on .NET 10, the current LTS release. We cover project setup, CRUD endpoints, Entity Framework Core, validation, error handling, JWT authentication, and the REST API best practices that separate a demo from production code.

By the end you'll have a working, testable API and — more importantly — you'll understand why each piece exists. Let's get started.

Why ASP.NET Core for REST APIs in 2026?

ASP.NET Core remains one of the fastest mainstream web frameworks, regularly topping the TechEmpower benchmarks. In 2026, the framework gives you two ways to build a REST API: Minimal APIs (concise, function-based endpoints) and Controller-based APIs (the classic MVC style). This tutorial focuses on Minimal APIs because they have matured significantly and are now the recommended starting point for new services, while still calling out where controllers shine.

  • Performance: Native AOT support and a lean request pipeline mean low latency and small memory footprint.
  • Cross-platform: Runs on Windows, Linux, and macOS, and deploys cleanly to Docker and Kubernetes.
  • Built-in essentials: Dependency injection, configuration, logging, and OpenAPI support ship in the box.

Prerequisites and project setup

Install the .NET 10 SDK and verify it from your terminal. Any editor works — Visual Studio 2026, VS Code with the C# Dev Kit, or JetBrains Rider.

dotnet --version
// 10.0.x

dotnet new webapi -n BookStore.Api --use-program-main false
cd BookStore.Api
dotnet run

The webapi template scaffolds a Minimal API project. Open Program.cs and you'll see how little code is needed to bootstrap a server. We'll build a small Book Store API as our running example.

Building your first REST API endpoints (CRUD)

REST is built around resources and HTTP verbs. For a books resource the mapping is conventional: GET reads, POST creates, PUT replaces, PATCH partially updates, and DELETE removes. Following this convention is the first REST API best practice — it makes your API predictable for every client.

Start with a model and a simple in-memory store so you can see the shape of the endpoints before we add a database.

// Models/Book.cs
public record Book(int Id, string Title, string Author, decimal Price);

// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi(); // OpenAPI/Swagger document
var app = builder.Build();

if (app.Environment.IsDevelopment())
    app.MapOpenApi();

var books = new List<Book>
{
    new(1, "The Pragmatic Programmer", "Hunt & Thomas", 39.99m),
    new(2, "Clean Code", "Robert C. Martin", 34.50m)
};

// Group related routes under one prefix
var group = app.MapGroup("/api/books");

group.MapGet("/", () => Results.Ok(books));

group.MapGet("/{id:int}", (int id) =>
    books.FirstOrDefault(b => b.Id == id) is { } book
        ? Results.Ok(book)
        : Results.NotFound());

group.MapPost("/", (Book book) =>
{
    books.Add(book);
    return Results.Created($"/api/books/{book.Id}", book);
});

group.MapPut("/{id:int}", (int id, Book updated) =>
{
    var index = books.FindIndex(b => b.Id == id);
    if (index == -1) return Results.NotFound();
    books[index] = updated with { Id = id };
    return Results.NoContent();
});

group.MapDelete("/{id:int}", (int id) =>
{
    var removed = books.RemoveAll(b => b.Id == id);
    return removed > 0 ? Results.NoContent() : Results.NotFound();
});

app.Run();

Notice the use of Results.Ok, Results.Created, Results.NotFound, and Results.NoContent. Returning the correct HTTP status code is not cosmetic — clients, caches, and monitoring tools all rely on it. A POST that succeeds should return 201 Created with a Location header, and a DELETE should return 204 No Content, not 200 with an empty body.

Adding Entity Framework Core for real persistence

An in-memory list is fine for learning, but a production REST API needs a database. Entity Framework Core is the standard ORM for .NET. We'll wire up EF Core CRUD against SQLite so the sample runs anywhere; switching to SQL Server or PostgreSQL is just a provider change.

dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
// Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options) { }

    public DbSet<Book> Books => Set<Book>();
}

// Make Book a mutable entity for EF Core
public class Book
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Author { get; set; } = string.Empty;
    public decimal Price { get; set; }
}

Register the DbContext in the DI container and create the database schema with a migration.

// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("Default")
        ?? "Data Source=books.db"));
dotnet tool install --global dotnet-ef
dotnet ef migrations add InitialCreate
dotnet ef database update

Now refactor the endpoints to use the database. Because EF Core operations are I/O bound, every database call should be async — this is critical for throughput, as it frees the thread to serve other requests while the query runs.

var group = app.MapGroup("/api/books");

group.MapGet("/", async (AppDbContext db) =>
    await db.Books.AsNoTracking().ToListAsync());

group.MapGet("/{id:int}", async (int id, AppDbContext db) =>
    await db.Books.FindAsync(id) is { } book
        ? Results.Ok(book)
        : Results.NotFound());

group.MapPost("/", async (Book book, AppDbContext db) =>
{
    db.Books.Add(book);
    await db.SaveChangesAsync();
    return Results.Created($"/api/books/{book.Id}", book);
});

Use AsNoTracking() for read-only queries. It tells EF Core to skip change-tracking overhead, which measurably reduces memory and CPU on high-traffic GET endpoints.

Input validation and the DTO pattern

Never expose your database entities directly to clients. Accepting a raw entity invites over-posting attacks, where a malicious caller sets fields they shouldn't (like Id or an admin flag). The fix is the DTO (Data Transfer Object) pattern: define separate request and response shapes.

// DTOs
public record CreateBookRequest(string Title, string Author, decimal Price);
public record BookResponse(int Id, string Title, string Author, decimal Price);

Validate incoming data before it touches your database. In .NET 10, Minimal APIs support validation via data annotations with a single call to AddValidation(), returning a clean 400 response automatically.

using System.ComponentModel.DataAnnotations;

public record CreateBookRequest(
    [Required, StringLength(200)] string Title,
    [Required, StringLength(100)] string Author,
    [Range(0.01, 10000)] decimal Price);

// Program.cs
builder.Services.AddValidation();

group.MapPost("/", async (CreateBookRequest req, AppDbContext db) =>
{
    var book = new Book { Title = req.Title, Author = req.Author, Price = req.Price };
    db.Books.Add(book);
    await db.SaveChangesAsync();
    var response = new BookResponse(book.Id, book.Title, book.Author, book.Price);
    return Results.Created($"/api/books/{book.Id}", response);
});

Global error handling with ProblemDetails

A production-ready API should never leak raw stack traces. ASP.NET Core supports RFC 9457 Problem Details, a standardized JSON error format. Wire it up once and every unhandled exception returns a consistent, machine-readable error.

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();   // converts exceptions to ProblemDetails
app.UseStatusCodePages();    // ProblemDetails for 4xx without bodies

This single piece of middleware is one of the most overlooked REST API best practices. Consistent error contracts mean client developers can handle failures programmatically instead of parsing free-text strings.

Securing the API with JWT authentication

Most real APIs require authentication. JWT (JSON Web Token) authentication in ASP.NET Core is the most common approach for stateless APIs. The client sends a bearer token in the Authorization header, and the server validates its signature on each request without storing session state.

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
// Program.cs
builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new()
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
        };
    });
builder.Services.AddAuthorization();

// after building the app
app.UseAuthentication();
app.UseAuthorization();

// protect write operations
group.MapDelete("/{id:int}", async (int id, AppDbContext db) =>
{
    var book = await db.Books.FindAsync(id);
    if (book is null) return Results.NotFound();
    db.Books.Remove(book);
    await db.SaveChangesAsync();
    return Results.NoContent();
}).RequireAuthorization();

Critical security note: never hard-code the signing key. Store secrets in environment variables, .NET User Secrets during development, or a managed vault (Azure Key Vault, AWS Secrets Manager) in production. The key must be long and random.

REST API best practices for production

Working endpoints are only half the job. These practices turn your tutorial project into a service you can confidently deploy.

  • Version your API: Prefix routes with /api/v1 so you can evolve the contract without breaking existing clients.
  • Paginate large collections: A GET /api/books that returns 100,000 rows will crater performance. Accept page and pageSize query parameters and cap the maximum page size.
  • Use async all the way down: Mixing blocking calls (.Result, .Wait()) into an async pipeline causes thread-pool starvation under load.
  • Add structured logging: Use ILogger with structured properties so logs are queryable in tools like Seq, Application Insights, or Grafana.
  • Enable rate limiting: The built-in rate limiter middleware protects you from abuse and runaway clients.
  • Document with OpenAPI: Ship the generated OpenAPI document so consumers can auto-generate clients and explore endpoints.

Common pitfalls to avoid

  • Returning entities instead of DTOs — leaks your schema and enables over-posting.
  • Ignoring HTTP status codes — returning 200 for everything, including errors, breaks clients and caches.
  • Forgetting AsNoTracking() on read queries — wastes memory and CPU at scale.
  • Putting secrets in appsettings.json — they end up in source control. Use User Secrets or a vault.
  • No global exception handling — unhandled errors leak stack traces to clients.

Testing your ASP.NET Core REST API

Before deployment, test the API. The fastest loop is the .http file that ships with the template, or a quick integration test using WebApplicationFactory.

// GET all books
GET https://localhost:5001/api/books
Accept: application/json

###
// POST a new book
POST https://localhost:5001/api/books
Content-Type: application/json

{
  "title": "Domain-Driven Design",
  "author": "Eric Evans",
  "price": 49.99
}

Conclusion and key takeaways

You've now completed an end-to-end ASP.NET Core REST API tutorial for 2026 — from an empty project to a secured, validated, database-backed Web API on .NET 10. More importantly, you understand the reasoning behind each decision, which is what lets you adapt these patterns to your own services.

  • Use Minimal APIs with route groups for clean, concise endpoints, and return correct HTTP status codes.
  • Persist data with Entity Framework Core, keeping read queries AsNoTracking() and all I/O async.
  • Protect your schema with the DTO pattern and validate every request.
  • Standardize failures with ProblemDetails and secure write operations with JWT authentication.
  • Apply production best practices — versioning, pagination, rate limiting, logging, and OpenAPI — before you ship.

The next step is deployment: containerize the API with Docker, add a CI/CD pipeline, and push to your cloud of choice. With the foundation you built here, your ASP.NET Core Web API is ready for production traffic. Happy coding!

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...