BrandGhost
ASP.NET Core Web API in .NET: The Complete Guide

ASP.NET Core Web API in .NET: The Complete Guide

ASP.NET Core Web API in .NET: The Complete Guide

Building HTTP services in .NET has never been more productive. ASP.NET Core Web API is Microsoft's framework for creating RESTful HTTP services consumed by browsers, mobile applications, desktop clients, and other backend services. It sits at the intersection of developer experience and raw performance -- and with .NET 10, it has never been faster or more capable.

This guide covers everything you need to know: how requests flow through the pipeline, how to set up routing and controllers, how to secure your API with JWT bearer tokens, how to handle errors with the ProblemDetails standard, and how to ship to production with Docker or Azure.

Why ASP.NET Core for Web APIs?

The case for ASP.NET Core is strong. It is cross-platform -- you can develop and deploy on Windows, Linux, or macOS without changing a line of code. It uses Kestrel, a high-performance asynchronous HTTP server that consistently ranks near the top of industry benchmarks like the TechEmpower framework benchmarks. And it ships with a built-in dependency injection container, so you never need a third-party IoC library just to get started.

.NET 10 raises the bar further. Native AOT support has improved significantly for Web API projects in .NET 10, though compatibility still depends on the app's feature usage and library choices -- a potential advantage for containerized workloads billed per second. Minimal APIs have matured into a viable alternative to controllers for microservices. The built-in OpenAPI support via Microsoft.AspNetCore.OpenApi eliminates a third-party dependency for many projects.

The framework can also handle the tedious plumbing for you. Model validation, content negotiation, RFC 9457 ProblemDetails error formatting, attribute-based routing, and comprehensive middleware infrastructure all come out of the box. You write business logic; ASP.NET Core handles the HTTP ceremony.

Architecture Overview: The Request Pipeline

Understanding the request pipeline is the foundation of everything else in ASP.NET Core. Every HTTP request enters through Kestrel and travels through a chain of middleware components before reaching your endpoint handler. Each middleware can inspect the request, modify the response, short-circuit the chain, or pass control to the next component.

Here is a typical Program.cs for a .NET 10 Web API project:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi();

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = builder.Configuration["Auth:Authority"];
        options.Audience = builder.Configuration["Auth:Audience"];
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromSeconds(30)
        };
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
});

builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

var app = builder.Build();

app.MapOpenApi();

// Exception handling must be first to catch exceptions from the full pipeline
app.UseExceptionHandler("/error");
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

The ordering of middleware registration matters enormously. UseAuthentication must precede UseAuthorization. Both must follow UseRouting -- which MapControllers handles implicitly in .NET 10. Reverse that order and your security policies will silently fail on every request.

The pipeline is essentially a linked list. Each middleware wraps the next one. When you call await next(context) inside a middleware, you hand control to the following component. This composability is what makes ASP.NET Core so extensible -- you can inject logging, caching, rate limiting, or any cross-cutting concern without touching your endpoint logic. For structured logging across the pipeline, read Logging in .NET: The Complete Developer's Guide for the full picture on logging infrastructure.

Routing: Mapping URLs to Handlers

Routing maps an incoming HTTP request URL and verb to a specific endpoint handler, extracting parameter values along the way. ASP.NET Core uses two main routing styles: conventional routing (primarily for MVC apps rendering HTML) and attribute routing (the standard for Web APIs).

Attribute routing is commonly preferred for Web APIs because route definitions live directly on the controller and action, making the API surface explicit and readable at a glance. The [HttpGet], [HttpPost], [HttpPut], [HttpDelete], and [HttpPatch] attributes define both the verb and the route template for each action.

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAll() => Ok(new[] { "Widget", "Gadget" });

    [HttpGet("{id:int}")]
    public IActionResult GetById(int id) => Ok(new { Id = id, Name = "Widget" });

    [HttpPost]
    public IActionResult Create([FromBody] CreateProductRequest request)
    {
        return CreatedAtAction(nameof(GetById), new { id = 42 }, request);
    }

    [HttpDelete("{id:int}")]
    public IActionResult Delete(int id) => NoContent();
}

public record CreateProductRequest(string Name, decimal Price);

The [controller] token in the route template is replaced at runtime with the controller's name minus the Controller suffix. Route constraints like {id:int} restrict the parameter to integers and return a 404 if the constraint is not satisfied. The framework has built-in constraints for guid, bool, datetime, decimal, regex patterns, length ranges, and numeric ranges. Routing is covered in depth in the dedicated routing article in this cluster.

Controllers vs Minimal APIs

ASP.NET Core gives you two primary ways to define endpoints: traditional controllers and minimal APIs. Both are valid choices, but they optimize for different scenarios.

Controllers -- classes that inherit from ControllerBase -- are usually the better fit for complex APIs with many endpoints, filters, and cross-cutting concerns. The [ApiController] attribute unlocks automatic model validation, binding source inference, and standardized ProblemDetails error responses. Controllers integrate naturally with action filters, areas, and OpenAPI documentation tools.

Minimal APIs map lambda expressions or method groups directly to routes. They have less ceremony, which makes them ideal for microservices, Azure Functions-style endpoints, or projects where you want the smallest possible startup. .NET 10 makes minimal APIs even more ergonomic with richer endpoint metadata and improved OpenAPI generation. The trade-off is that advanced features like action filters require more manual wiring compared to the controller pipeline.

For a greenfield API with more than a handful of endpoints, controllers tend to scale better in terms of code organization. Teams that already know the MVC controller model have a shorter learning curve.

Authentication and Authorization

Security is non-negotiable. ASP.NET Core has first-class support for JWT bearer authentication, cookie authentication, API key schemes, OAuth 2.0, and OpenID Connect. For most modern Web APIs, JWT bearer tokens issued by an identity provider -- Azure Active Directory, Auth0, or a self-hosted IdentityServer -- are the standard approach.

Authorization in ASP.NET Core is policy-based. You define policies in AddAuthorization(...) and apply them with [Authorize(Policy = "PolicyName")]. Policies can require specific roles, claims, scopes, or custom requirements you implement yourself. This is far more maintainable than scattering [Authorize(Roles = "Admin")] attributes across dozens of controllers.

[ApiController]
[Route("api/[controller]")]
[Authorize]
public class OrdersController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAll() => Ok(Array.Empty<object>());

    [HttpGet("{id:guid}")]
    public IActionResult GetById(Guid id) => Ok(new { Id = id });

    [HttpPost]
    [Authorize(Policy = "AdminOnly")]
    public IActionResult Create([FromBody] CreateOrderRequest request)
        => CreatedAtAction(nameof(GetById), new { id = Guid.NewGuid() }, request);

    [HttpDelete("{id:guid}")]
    [AllowAnonymous]
    public IActionResult Delete(Guid id) => NoContent();
}

public record CreateOrderRequest(string CustomerId, decimal Total);

[Authorize] on the controller class protects all actions. [Authorize(Policy = "AdminOnly")] on a specific action applies an additional policy requirement. [AllowAnonymous] overrides the class-level authorization for endpoints that should be public. For production secrets management, use Azure Key Vault or ASP.NET Core Data Protection rather than storing JWT signing keys in appsettings.json.

API Versioning

APIs evolve. Clients cannot always update in lockstep with your server. API versioning lets you introduce breaking changes without disrupting existing consumers.

The three common approaches are URL versioning (/api/v1/products), query string versioning (/api/products?api-version=1.0), and header versioning (Api-Version: 1.0). URL versioning is the most visible and easiest to test in a browser or with curl. The Asp.Versioning.Http NuGet package (third-party, not part of the framework; previously Microsoft.AspNetCore.Mvc.Versioning) provides clean support for all three approaches and integrates with OpenAPI to generate per-version documentation automatically.

Introducing versioning early is far easier than retrofitting it onto a mature API. Even if you never publish a v2, starting with api/v1/ signals to consumers that you have thought about this and gives you a clear path forward.

Error Handling and ProblemDetails

Inconsistent error responses are a consumer's nightmare. RFC 9457 defines the ProblemDetails format -- a standard JSON structure that describes HTTP API errors. ASP.NET Core supports this out of the box and [ApiController] activates it automatically for 400 responses.

The IExceptionHandler interface (introduced in .NET 8) combined with AddExceptionHandler<T>() and UseExceptionHandler() gives you centralized exception handling with full control over the response format:

public sealed class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;

    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
    {
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        _logger.LogError(exception, "Unhandled exception: {Message}", exception.Message);

        var (status, title) = exception switch
        {
            NotFoundException => (StatusCodes.Status404NotFound, "Resource not found"),
            ValidationException => (StatusCodes.Status422UnprocessableEntity, "Validation failed"),
            _ => (StatusCodes.Status500InternalServerError, "An unexpected error occurred")
        };

        var problemDetails = new ProblemDetails
        {
            Status = status,
            Title = title,
            Detail = exception.Message
        };

        httpContext.Response.StatusCode = status;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);

        return true;
    }
}

All unhandled exceptions flow through your handler and produce a consistent JSON response. Clients get a predictable shape with type, title, status, detail, and instance fields -- no more guessing whether the error response is a plain string, a custom JSON object, or an HTML page.

Model Validation

The [ApiController] attribute automatically validates incoming request models and returns a 400 Bad Request with a ProblemDetails body if validation fails -- without you writing a single line of validation code in your action methods. Data annotations on your request models drive the validation rules.

Annotate your request record or class with attributes like [Required], [MaxLength(200)], [Range(0.01, 999999.99)], and [Url]. If a client sends a request with a missing required field or an out-of-range value, ASP.NET Core immediately returns a 400 with a descriptive ProblemDetails body listing each field error. No if (!ModelState.IsValid) guards needed in action methods. For complex validation logic that cannot be expressed with annotations -- cross-field validation, database uniqueness checks -- FluentValidation integrates cleanly with ASP.NET Core's validation pipeline via its IValidator<T> abstraction.

Middleware for Cross-Cutting Concerns

Middleware is the right abstraction for concerns that apply broadly across many endpoints: correlation IDs, rate limiting, response compression, HTTPS redirection, and request/response logging. Writing custom middleware is straightforward. Implement a class with an InvokeAsync(HttpContext context, RequestDelegate next) method and register it with app.UseMiddleware<YourMiddleware>().

For structured per-request logging, Serilog's request logging middleware is a natural fit. The Serilog in .NET guide and the dedicated Serilog in ASP.NET Core setup guide both cover how to plug Serilog's request enrichment into the ASP.NET Core pipeline, capturing HTTP method, path, status code, and elapsed time on every request automatically.

The Mediator design pattern pairs naturally with Web API controllers. Your action methods become thin dispatchers that send commands and queries through a mediator, keeping controllers focused on HTTP concerns and pushing business logic into dedicated handler classes. This is sometimes called the CQRS-over-HTTP pattern and scales extremely well as your API grows.

Integration Testing

Integration testing with WebApplicationFactory<TProgram> is the gold standard for ASP.NET Core APIs. It spins up your entire application in memory -- including middleware, DI registrations, routing, and serialization -- and lets you make real HTTP requests with HttpClient. This approach catches issues that isolated unit tests miss: routing misconfigurations, middleware ordering bugs, and serialization edge cases.

public class ProductsControllerTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public ProductsControllerTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                services.AddSingleton<IProductService, FakeProductService>();
            });
        }).CreateClient();
    }

    [Fact]
    public async Task GetProducts_ReturnsOk()
    {
        var response = await _client.GetAsync("/api/products");
        response.StatusCode.ShouldBe(HttpStatusCode.OK);
    }

    [Fact]
    public async Task CreateProduct_InvalidBody_Returns400()
    {
        var body = new StringContent("{}", Encoding.UTF8, "application/json");
        var response = await _client.PostAsync("/api/products", body);
        response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
    }
}

Run these tests in CI on every pull request. Catching a broken API contract before it reaches production is usually cheaper than fixing it after consumers report errors.

Deployment: Docker, Azure, and Native AOT

ASP.NET Core applications containerize cleanly. The official mcr.microsoft.com/dotnet/aspnet:10.0 base image is the starting point. Combined with Native AOT in .NET 10, you can produce a self-contained binary image with sub-second startup -- a significant win for container deployments where cold starts matter. The .NET 10 SDK's PublishAot property automates this.

Azure App Service is the simplest deployment target: push your code or Docker image, configure connection strings as App Settings, and you are done. For more advanced scenarios -- autoscaling, sidecar containers, traffic splitting -- Azure Container Apps offers a managed Kubernetes-like environment without operational overhead.

If your project is growing into a larger distributed system, understanding architectural patterns is crucial before you scale. Monolith architecture in C# covers the design decisions that apply when starting out, while modular monolith architecture in C# explores a well-structured middle ground between a single monolith and full microservices. Both patterns have direct implications for how you structure your ASP.NET Core Web API projects.

Conclusion

ASP.NET Core Web API is a mature, high-performance, and extremely productive framework for building HTTP services. It handles the hard parts -- routing, validation, serialization, authentication, error formatting, middleware composition -- while staying out of your way when you need to build custom solutions.

This guide covered the full landscape. The request pipeline and middleware ordering. Attribute routing and route constraints. Controllers versus minimal APIs. JWT authentication and policy-based authorization. ProblemDetails error handling. Automatic model validation. Integration testing with WebApplicationFactory. And deployment options ranging from Docker containers to Azure Container Apps. Each of these topics has a dedicated depth article in this cluster. Start with the sections most relevant to your current problem and follow the links to go deeper.

The best ASP.NET Core Web API is one that is consistent, predictable, and easy to consume. The framework gives you the tools. What you build with them is up to you.

Frequently Asked Questions

What is the difference between ASP.NET Core Web API and ASP.NET Core MVC?

ASP.NET Core MVC is designed for server-side web applications that render HTML views. It includes Razor view rendering, tag helpers, ViewData, TempData, and the full Controller base class. ASP.NET Core Web API is focused on returning structured data (JSON, XML) to non-browser clients -- mobile apps, JavaScript frontends, other services. For Web API work, you inherit from ControllerBase rather than Controller, which excludes the view engine overhead.

Both share the same middleware pipeline, routing engine, and dependency injection infrastructure. The choice is driven by whether your application renders HTML server-side or returns data. For anything consumed by a SPA, mobile app, or backend service, Web API is often the better fit.

Should I use controllers or minimal APIs?

For production APIs with more complex routing and a larger number of endpoints, many teams find controllers easier to maintain -- though this is a contextual judgment. Controllers offer built-in support for action filters, model binding, validation, and OpenAPI integration. The [ApiController] attribute alone saves significant boilerplate, and the controller model is familiar to most .NET developers.

Minimal APIs shine for small services where you want minimal ceremony: Azure Functions-style endpoints, proxy services, or projects where startup performance is critical. .NET 10 significantly narrowed the feature gap between the two. ASP.NET Core lets you mix both in the same application, so you are not locked in at project creation time.

How do I handle errors consistently across my Web API?

The RFC 9457 ProblemDetails standard is the right foundation. Define specific exception types for domain errors -- NotFoundException, ConflictException, ValidationException -- and handle them in a global IExceptionHandler. This keeps action methods clean and ensures every error produces a consistent response shape.

Avoid try-catch blocks inside action methods for predictable failures. Action methods handle the happy path; unexpected errors bubble to the global handler. Expected failures can throw domain exceptions or be returned as Result<T> from the service layer. Consistency is the goal -- consumers should write one error-handling routine that works for every endpoint.

How does dependency injection work in ASP.NET Core Web APIs?

ASP.NET Core ships with a built-in DI container that supports constructor injection in controllers, middleware, filters, and minimal API handlers. You register services in builder.Services using AddSingleton, AddScoped, or AddTransient depending on the desired lifetime. Controllers resolve their constructor parameters automatically when activated per request.

Scoped services live for the duration of a single HTTP request and are disposed when the request ends. If you are curious about how the container resolves dependencies at runtime, How DI Containers Use Reflection Internally explains the reflection and expression tree mechanics under the hood.

What is the best way to secure an ASP.NET Core Web API in .NET 10?

JWT bearer authentication is the standard for APIs consumed by JavaScript clients, mobile apps, and other services. Your API validates the token's signature, issuer, audience, and expiration on every request. No server-side session state is needed -- the token carries the claims. Apply the principle of least privilege and use policy-based authorization to express what a caller must possess rather than what they must be.

For production secrets, use Azure Key Vault or AWS Secrets Manager. Never put JWT signing keys or connection strings in appsettings.json in source control.

How do I version an ASP.NET Core Web API without breaking existing clients?

URL path versioning with a prefix like /api/v1/ is often the safer option. It is explicit, requires no special headers, and every HTTP tool understands it. Add it from day one -- retrofitting it onto a mature API is painful. When introducing breaking changes, create a new version and keep the old one running long enough for clients to migrate.

The Asp.Versioning.Http NuGet package automates routing and controller selection for versioning, and generates per-version OpenAPI documents. Non-breaking additive changes do not require a version bump -- reserve version increments for changes that force clients to update.

How do I write integration tests for an ASP.NET Core Web API?

Use WebApplicationFactory<TProgram> from Microsoft.AspNetCore.Mvc.Testing. It boots your entire application in memory and provides an HttpClient for real HTTP requests through the full middleware and routing pipeline. This catches issues that unit tests miss: routing misconfigurations, middleware ordering bugs, and serialization edge cases.

Override service registrations to replace real databases with in-memory alternatives. Use SQLite in-memory databases for repository tests so they run fast and in isolation. Run the full suite in CI on every pull request.


For official framework documentation, see the ASP.NET Core documentation on Microsoft Learn.

ASP.NET Core for Beginners - What You Need To Get Started

Interested in building web applications? ASP.NET Core is a powerful dotnet tech stack for just that! Here are all of the details for ASP.NET Core for beginners!

How to Build An ASP.NET Core Web API: A Practical Beginner's Tutorial

Learn how to build an ASP.NET core web API! This tutorial for beginners will guide you through setting up the project to building the API endpoints.

Build A Web API From Scratch - Dev Leader Weekly 57

Welcome to another issue of Dev Leader Weekly! In this issue, I discuss the basic building blocks for building a Web API in DotNet!

An error has occurred. This application may no longer respond until reloaded. Reload