Learn how to build a production-ready ASP.NET Core REST API in 2026. Step-by-step C# tutorial with CRUD, EF Core, and best practices. Start coding now!
If you searched for an ASP.NET Core REST API tutorial, you want more than a "Hello World" endpoint — you want to ship something real. In this 2026 guide you will build a production-ready ASP.NET Core Web API from scratch: clean CRUD endpoints, Entity Framework Core persistence, validation, versioning, authentication, and the small architectural decisions that separate a demo from a deployable service. Every code sample is runnable on .NET 9, and by the end you will understand not just how each piece works, but why it belongs in a professional API.
This tutorial targets three readers at once: beginners learning how to build a REST API in C#, intermediate developers looking for ASP.NET Core best practices, and seniors who want the advanced patterns. Skim to your level, or read straight through.
Why ASP.NET Core for REST APIs in 2026?
ASP.NET Core remains one of the fastest managed web frameworks in the world, routinely near the top of the TechEmpower benchmarks. It is cross-platform (Windows, Linux, macOS, containers), has first-class dependency injection built in, and ships with everything you need for a modern REST API: routing, model binding, validation, OpenAPI support, and middleware. For teams already invested in C#, it means one language across the stack and a mature tooling story with Visual Studio, VS Code, and the `dotnet` CLI.
REST (Representational State Transfer) is still the default architectural style for web APIs because it maps cleanly to HTTP: resources are nouns (URLs), and HTTP verbs (GET, POST, PUT, PATCH, DELETE) are the actions. Understanding this mapping is the foundation of every good API design.
Setting Up Your ASP.NET Core Web API Project
Make sure you have the .NET 9 SDK installed (run dotnet --version). Then scaffold and run a new Web API in three commands:
// Terminal commands
dotnet new webapi -n ProductApi --use-controllers
cd ProductApi
dotnet run
The --use-controllers flag gives you controller-based routing, which scales better than the minimal-API style for larger applications with many endpoints and shared conventions. Your project already includes OpenAPI support, so you get machine-readable API documentation out of the box.
The Program.cs entry point
In modern .NET, Program.cs uses top-level statements to wire up services and middleware. Here is a clean starting point:
var builder = WebApplication.CreateBuilder(args);
// Register services (the DI container)
builder.Services.AddControllers();
builder.Services.AddOpenApi();
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("Default")));
var app = builder.Build();
// Configure the HTTP request pipeline (order matters!)
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Why order matters: middleware runs top-to-bottom on the way in and bottom-to-top on the way out. Authentication must come before authorization, and both must precede MapControllers, or your security checks silently do nothing.
Designing the Data Model and EF Core Context
A REST API represents resources. Ours is a Product. Keep entities focused and use data annotations for validation that the framework enforces automatically.
using System.ComponentModel.DataAnnotations;
public class Product
{
public int Id { get; set; }
[Required, StringLength(100)]
public string Name { get; set; } = string.Empty;
[Range(0.01, 100000)]
public decimal Price { get; set; }
public int StockQuantity { get; set; }
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
}
The Entity Framework Core DbContext is your gateway to the database. It tracks changes and translates LINQ into SQL:
using Microsoft.EntityFrameworkCore;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
public DbSet<Product> Products => Set<Product>();
}
Create and apply a migration so EF Core builds the schema for you:
dotnet ef migrations add InitialCreate
dotnet ef database update
Building CRUD Endpoints the Right Way
Here is the heart of the tutorial: a controller exposing full CRUD operations. Notice the use of DTOs, correct HTTP status codes, async database calls, and CreatedAtAction for proper REST semantics.
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
[ApiController]
[Route("api/v1/[controller]")]
public class ProductsController : ControllerBase
{
private readonly AppDbContext _db;
public ProductsController(AppDbContext db) => _db = db;
// GET api/v1/products?page=1&pageSize=20
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetAll(
int page = 1, int pageSize = 20)
{
var items = await _db.Products
.OrderBy(p => p.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return Ok(items);
}
// GET api/v1/products/5
[HttpGet("{id:int}")]
public async Task<ActionResult<Product>> GetById(int id)
{
var product = await _db.Products.FindAsync(id);
return product is null ? NotFound() : Ok(product);
}
// POST api/v1/products
[HttpPost]
public async Task<ActionResult<Product>> Create(ProductCreateDto dto)
{
var product = new Product { Name = dto.Name, Price = dto.Price };
_db.Products.Add(product);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(GetById),
new { id = product.Id }, product);
}
// PUT api/v1/products/5
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(int id, ProductCreateDto dto)
{
var product = await _db.Products.FindAsync(id);
if (product is null) return NotFound();
product.Name = dto.Name;
product.Price = dto.Price;
await _db.SaveChangesAsync();
return NoContent();
}
// DELETE api/v1/products/5
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id)
{
var product = await _db.Products.FindAsync(id);
if (product is null) return NotFound();
_db.Products.Remove(product);
await _db.SaveChangesAsync();
return NoContent();
}
}
public record ProductCreateDto(string Name, decimal Price);
Why DTOs? Never bind requests directly to your EF entities. A Data Transfer Object controls exactly which fields a client can set, preventing "over-posting" attacks where a malicious user sets fields like Id or an admin flag. It also decouples your public contract from your database schema, so internal changes don't break API consumers.
Why CreatedAtAction? A correct POST returns HTTP 201 Created plus a Location header pointing at the new resource. This is proper REST semantics and helps clients discover the URL of what they just created.
Why async everywhere? Database and network calls are I/O-bound. Async methods release the request thread while waiting, so your server handles far more concurrent requests — critical for throughput under load.
Validation and Global Error Handling
Because the controller is decorated with [ApiController], model validation runs automatically: if ProductCreateDto violates a data annotation, the framework returns a structured 400 response before your code even executes. Don't fight this — lean on it.
For unexpected exceptions, a production API should never leak stack traces. Use the built-in problem-details handler to return consistent, RFC 7807 error responses:
builder.Services.AddProblemDetails();
// In the pipeline:
app.UseExceptionHandler();
app.UseStatusCodePages();
This gives every error a predictable JSON shape (type, title, status, detail), which client developers will thank you for.
Securing Your API with JWT Authentication
Most production APIs need authentication. The industry standard for stateless REST APIs is the JWT (JSON Web Token) bearer scheme. Register it once:
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.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"]!))
};
});
Then protect any endpoint or controller with a single attribute:
[Authorize]
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id) { /* ... */ }
Why JWT? Tokens are self-contained and verified with a signing key, so your API stays stateless — no server-side session store, which makes horizontal scaling across containers trivial. Keep the signing key in a secret manager (Azure Key Vault, AWS Secrets Manager, or user-secrets in development), never in source control.
ASP.NET Core Best Practices and Common Pitfalls
These are the lessons that turn a working API into a production-ready one:
- Version your API from day one. The
api/v1/prefix (or a versioning library) lets you evolve without breaking existing clients. - Always paginate collection endpoints. Returning every row is a performance and memory disaster once your table grows — the pagination shown above is non-negotiable.
- Use
AsNoTracking()for read-only queries. It skips EF Core's change tracker and measurably speeds up GET requests. - Return DTOs, not entities, from GETs too. This prevents accidentally serializing navigation properties and leaking internal data or causing circular-reference errors.
- Don't call
.Resultor.Wait()on async code. This blocks threads and can deadlock — alwaysawait. - Enable HTTPS and CORS deliberately. Configure a specific CORS policy rather than
AllowAnyOriginin production. - Add health checks and structured logging.
app.MapHealthChecks("/health")plus Serilog gives you the observability real deployments need. - Avoid the "fat controller." As logic grows, move business rules into services injected via DI, keeping controllers thin and testable.
Testing and Deploying Your REST API
Use the generated OpenAPI document (or a tool like Scalar or the .http files in Visual Studio) to exercise every endpoint. For automated confidence, WebApplicationFactory<T> lets you write fast integration tests that spin up the whole pipeline in memory. When you're ready to ship, containerize with a simple Dockerfile and deploy to Azure App Service, AWS, or any Kubernetes cluster — ASP.NET Core runs identically everywhere.
Conclusion: Key Takeaways
You've completed a full ASP.NET Core REST API tutorial and built a service with real production concerns baked in. Here's what to remember:
- Map resources to HTTP verbs and return correct status codes (200, 201, 204, 400, 404) — that's the essence of REST.
- Use DTOs to protect your data model and control your public contract.
- Go async end-to-end with Entity Framework Core for scalable, high-throughput I/O.
- Secure with JWT, validate automatically via
[ApiController], and return consistent problem-details errors. - Apply the best practices — versioning, pagination, no-tracking reads, and thin controllers — to keep the API maintainable as it grows.
Start with the CRUD controller above, then layer in authentication and observability as your ASP.NET Core Web API matures. Bookmark this guide, fire up dotnet run, and ship your production-ready REST API today.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment