ASP.NET Core REST API Best Practices

Introduction

Building an ASP.NET Core REST API that works is relatively easy. Building one that remains predictable, secure, maintainable, and performant as traffic and business requirements grow requires more deliberate architecture.

Good REST API design is not only about endpoint naming. It includes resource modeling, HTTP semantics, validation, authentication, authorization, error handling, pagination, database access, observability, and deployment.

This guide focuses on practical ASP.NET Core patterns that help an API remain maintainable as the application grows.

Design Resources Around Business Concepts

Prefer resource-oriented endpoints:

GET    /api/orders
GET    /api/orders/{id}
POST   /api/orders
PUT    /api/orders/{id}
DELETE /api/orders/{id}

Avoid designing the entire API around implementation details such as:

/api/getAllOrders
/api/createNewOrder
/api/deleteOrder

The HTTP method already communicates the operation.

For commands that do not naturally map to CRUD, use explicit actions where they represent a meaningful business operation:

POST /api/orders/{id}/cancel
POST /api/orders/{id}/approve

Use DTOs at the API Boundary

Do not expose EF Core entities directly from controllers.

Use request and response DTOs:

public sealed record CreateOrderRequest(
    Guid CustomerId,
    IReadOnlyCollection<CreateOrderItemRequest> Items);

public sealed record OrderResponse(
    Guid Id,
    Guid CustomerId,
    decimal Total,
    string Status);

This prevents persistence concerns from becoming part of the public API contract and gives the API room to evolve independently of the database model.

Keep Controllers Thin

Controllers or Minimal API endpoints should coordinate HTTP concerns rather than contain business rules.

A typical flow is:

HTTP Request
    ↓
Endpoint / Controller
    ↓
Application Service / Handler
    ↓
Domain / Business Rules
    ↓
Repository / EF Core
    ↓
Database

Validation, authorization, business rules, persistence, and response mapping should have clear responsibilities.

Validate Requests at the Boundary

Use FluentValidation or another validation approach appropriate to the application.

For example:

public sealed class CreateOrderValidator
    : AbstractValidator<CreateOrderRequest>
{
    public CreateOrderValidator()
    {
        RuleFor(x => x.CustomerId)
            .NotEmpty();

        RuleFor(x => x.Items)
            .NotEmpty();

        RuleForEach(x => x.Items)
            .ChildRules(item =>
            {
                item.RuleFor(x => x.ProductId)
                    .NotEmpty();

                item.RuleFor(x => x.Quantity)
                    .GreaterThan(0);
            });
    }
}

Validation should reject invalid input before it reaches deeper application layers.

Return Consistent Errors

Use a consistent problem-details representation rather than returning arbitrary strings from every endpoint.

ASP.NET Core supports RFC 7807-style problem details through its built-in infrastructure.

A client should be able to distinguish:

  • validation errors
  • authentication failures
  • authorization failures
  • missing resources
  • conflicts
  • unexpected server errors

Do not expose stack traces or database exceptions to clients.

Use HTTP Status Codes Consistently

Typical responses include:

Situation Status
Successful read 200 OK
Resource created 201 Created
Successful command with no body 204 No Content
Invalid request 400 Bad Request
Unauthenticated 401 Unauthorized
Forbidden 403 Forbidden
Resource not found 404 Not Found
Business conflict 409 Conflict
Unexpected server error 500 Internal Server Error

The exact response should follow the API contract rather than being chosen arbitrarily.

Authentication and Authorization

Authentication establishes identity. Authorization determines whether that identity can perform an operation.

For JWT-based APIs, validate:

  • issuer
  • audience
  • signature
  • expiration
  • required claims

Use policies for business authorization rather than scattering role checks throughout controllers.

[Authorize(Policy = "Orders.Read")]
[HttpGet("{id:guid}")]
public async Task<ActionResult<OrderResponse>> Get(
    Guid id,
    CancellationToken cancellationToken)
{
    ...
}

Keep authorization decisions close to the application boundary, while keeping sensitive business rules enforceable deeper in the application where necessary.

Use EF Core Efficiently

Avoid loading entire tables when an endpoint needs a small projection.

Prefer:

var orders = await dbContext.Orders
    .AsNoTracking()
    .Where(x => x.CustomerId == customerId)
    .Select(x => new OrderResponse(
        x.Id,
        x.CustomerId,
        x.Total,
        x.Status))
    .ToListAsync(cancellationToken);

This reduces unnecessary entity materialization and makes the database query match the API response.

Use AsNoTracking() for read-only queries where tracking is not required.

Avoid accidental N+1 queries and inspect generated SQL for important endpoints.

Pagination Is a Contract

Do not return an unbounded collection from a production API.

A simple offset-based contract can expose:

GET /api/orders?page=2&pageSize=50

For large or frequently changing datasets, keyset/cursor pagination can be more appropriate.

The choice depends on the access pattern and consistency requirements.

Use CancellationToken

Propagate cancellation through asynchronous operations:

public async Task<IResult> GetOrders(
    CancellationToken cancellationToken)
{
    var result = await service.GetOrdersAsync(cancellationToken);

    return Results.Ok(result);
}

This helps stop unnecessary work when the client disconnects or the request is cancelled.

Observability Matters

Production APIs need structured logging, metrics, and tracing.

Serilog or the built-in Microsoft.Extensions.Logging abstractions can provide structured logs.

Log useful context such as:

  • request correlation
  • endpoint
  • duration
  • status code
  • relevant identifiers

Do not log passwords, access tokens, secrets, or unnecessary personal data.

Distributed tracing becomes increasingly useful when requests cross multiple services.

Rate Limiting and Abuse Protection

Public or sensitive APIs should consider rate limiting.

ASP.NET Core provides rate-limiting middleware that can be configured according to the application’s traffic model.

Rate limiting should complement, not replace, authentication and authorization.

Version APIs Deliberately

API versioning should be driven by contract compatibility rather than by a desire to version every deployment.

If breaking changes are necessary, establish a clear versioning strategy and communicate deprecation timelines.

Keep Configuration Outside Code

Use configuration and environment variables for environment-specific values.

Secrets should come from an appropriate secret-management system rather than source control.

Practical Checklist

Before calling an API production-ready, verify:

  • DTOs separate API and persistence models
  • Input validation is centralized
  • Authorization policies are defined
  • Errors use a consistent contract
  • Queries are projected efficiently
  • Pagination exists for large collections
  • Cancellation is propagated
  • Logging is structured
  • Sensitive information is not logged
  • Rate limiting is considered
  • Database queries have appropriate indexes
  • API contracts are documented
  • Health checks and observability are available

Conclusion

The best ASP.NET Core REST API practices are less about individual framework features and more about establishing clear boundaries.

Keep HTTP concerns at the API boundary, business rules in the application/domain layers, and persistence concerns behind a deliberate data-access boundary. Combine consistent contracts with efficient EF Core queries, strong authorization, validation, observability, and operational controls.

The result is an API that is easier to evolve without turning every new requirement into a change across the entire application.

Let's connect

Interested in discussing software architecture, engineering challenges or potential collaboration? Feel free to get in touch.

© 2026 Rajdip Khavad. All rights reserved.