ASP.NET Core & API Development

ASP.NET Core REST API Best Practices:
Designing Scalable Enterprise APIs

Practical techniques for designing secure, maintainable and high-performance ASP.NET Core REST APIs for enterprise applications.

August 2026 • 14 min read • By Kshitij Mehta

ASP.NET Core REST API best practices become increasingly important as a web API grows from a small application into an enterprise platform serving multiple clients, integrations and business workflows. Good API design is not only about creating controllers and returning JSON. It also involves resource modeling, validation, security, versioning, error handling, observability, performance and clear application boundaries.

Enterprise systems such as ShippingPortal illustrate why these concerns matter. A single business workflow can expose APIs for shipments, tracking, customers, addresses and carrier services while also communicating with SQL Server and external carrier APIs. The API layer needs to remain stable even when underlying database queries or third-party integrations change.

This guide presents practical ASP.NET Core REST API best practices for C# and .NET applications, using enterprise shipping workflows as examples while keeping the principles applicable to finance, retail, logistics, ERP and other business systems.

Key Takeaways
  • Design APIs around business resources and predictable contracts.
  • Keep controllers thin and application workflows explicit.
  • Validate input before it reaches business and data-access code.
  • Use consistent authentication, authorization and error responses.
  • Design for pagination, cancellation, logging and observability from the start.
  • Keep external carrier and database implementations behind application boundaries.

1. Design REST APIs Around Resources

A REST API is easier to consume when its URLs and HTTP methods describe stable business resources. In a shipping application, examples might include shipments, orders, addresses, customers and tracking records.


GET    /api/shipments
GET    /api/shipments/123
POST   /api/shipments
PUT    /api/shipments/123
DELETE /api/shipments/123
                

Prefer resource-oriented names over URLs that expose internal implementation details. The API should communicate what the client is operating on rather than which database table happens to store it.


2. Use HTTP Methods Consistently

Consistent HTTP semantics make an API easier to understand and integrate. Use GET for retrieval, POST for creating a resource or initiating a non-idempotent operation, PUT for replacing a resource when appropriate, PATCH for partial updates and DELETE for deletion.


GET    /api/shipments/123
POST   /api/shipments
PATCH  /api/shipments/123
DELETE /api/shipments/123
                

The exact behavior should be documented and consistent across the API rather than changing from endpoint to endpoint.


3. Keep ASP.NET Core Controllers Thin

Controllers should primarily handle HTTP concerns: receiving the request, invoking the application workflow and returning the appropriate response.


[ApiController]
[Route("api/shipments")]
public sealed class ShipmentsController : ControllerBase
{
    private readonly IShipmentService _service;

    public ShipmentsController(
        IShipmentService service)
    {
        _service = service;
    }

    [HttpGet("{id:int}")]
    public async Task<ActionResult<ShipmentResponse>> Get(
        int id,
        CancellationToken cancellationToken)
    {
        var shipment =
            await _service.GetAsync(
                id,
                cancellationToken);

        if (shipment is null)
            return NotFound();

        return Ok(shipment);
    }
}
                

Business rules, carrier integration and database access should not be embedded throughout controller actions.


4. Use API Contracts Instead of Exposing Database Entities

Database entities are designed for persistence. API contracts are designed for clients. Keeping them separate allows internal database changes without unnecessarily changing a public API.


public sealed record ShipmentResponse(
    int Id,
    string Reference,
    string TrackingNumber,
    string Status);
                

This approach is particularly useful when SQL Server structures contain fields that should not be exposed to API consumers.


5. Validate API Requests at the Boundary

Request validation should happen as early as practical. Reject malformed or incomplete requests before they travel through business services and database code.


public sealed record CreateShipmentRequest(
    int OrderId,
    string CarrierCode,
    string ServiceCode);
                

Validation can cover required values, length limits, acceptable formats and business-specific requirements.


if (request.OrderId <= 0)
{
    return BadRequest(
        new
        {
            error = "A valid order identifier is required."
        });
}
                

6. Return Consistent HTTP Status Codes

Clients should be able to interpret API responses without learning a different convention for every endpoint.


200 OK
201 Created
204 No Content
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
409 Conflict
422 Unprocessable Entity
429 Too Many Requests
500 Internal Server Error
503 Service Unavailable
                

Not every API needs every status code, but the chosen conventions should be applied consistently.


7. Use a Consistent API Error Format

Error responses are part of the API contract. A consistent response makes it easier for client applications and support systems to process failures.


{
    "title": "Shipment validation failed",
    "status": 422,
    "detail": "The selected service is not available.",
    "traceId": "00-example-trace-id"
}
                

ASP.NET Core's problem-details support can provide a consistent foundation for API error responses.


8. Secure Enterprise APIs with Authentication and Authorization

Authentication answers who the caller is. Authorization determines what the caller is allowed to do. Keep those concerns separate from the business workflow itself.


[Authorize(Policy = "ShipmentRead")]
[HttpGet("{id:int}")]
public async Task<ActionResult<ShipmentResponse>> Get(
    int id,
    CancellationToken cancellationToken)
{
    var shipment =
        await _service.GetAsync(
            id,
            cancellationToken);

    return shipment is null
        ? NotFound()
        : Ok(shipment);
}
                

Policies are often preferable to scattering role checks throughout business code because the authorization requirement remains explicit at the API boundary.


9. Treat All Client Input as Untrusted

Query strings, route values, request bodies and uploaded files should all be treated as untrusted input.

  • Validate identifiers and ranges.
  • Limit request sizes where appropriate.
  • Use parameterized SQL for database values.
  • Validate uploaded files and external payloads.
  • Avoid exposing internal exception details to clients.

Security validation should complement database constraints, authorization and application-level business validation.


10. Paginate Large API Results

Enterprise APIs should not return thousands of shipments, transactions or tracking events simply because the database can produce them.


GET /api/shipments?page=1&pageSize=50
                

The API can return a defined page size and pagination metadata.


{
    "items": [
        {
            "id": 101,
            "reference": "SHIP-1001",
            "status": "Processed"
        }
    ],
    "page": 1,
    "pageSize": 50,
    "totalCount": 1542
}
                

11. Design Filtering and Sorting Predictably

Search-heavy enterprise endpoints should use explicit filter and sort parameters instead of requiring clients to create arbitrary SQL-like expressions.


GET /api/shipments?
    status=Processed
    &carrier=DHL
    &sort=createdDate
    &direction=desc
                

Validate supported values and map them to known application queries so the database workload remains predictable.


12. Support Cancellation for Long-Running API Operations

ASP.NET Core can receive a cancellation token when a client disconnects or a request is otherwise cancelled. Pass that token through application, database and integration layers where supported.


public async Task<ShipmentResponse> CreateAsync(
    CreateShipmentRequest request,
    CancellationToken cancellationToken)
{
    return await _shipmentService.CreateAsync(
        request,
        cancellationToken);
}
                

Cancellation is especially useful for long-running database operations and external API calls.


13. Use Asynchronous I/O in ASP.NET Core APIs

Database and HTTP operations are I/O-bound. Use asynchronous APIs rather than blocking request threads while waiting for SQL Server or an external service.


var shipment =
    await repository.GetAsync(
        shipmentId,
        cancellationToken);

var carrierResult =
    await carrierClient.CreateShipmentAsync(
        request,
        cancellationToken);
                

Avoid synchronous waits such as .Result and .Wait() in request-processing code.


14. Keep Data Access Behind Application Boundaries

An API controller should not need to know whether a shipment is loaded through Dapper, Entity Framework Core or direct ADO.NET.


Controller
    ↓
Application Service
    ↓
Repository / Data Access
    ↓
SQL Server
                

This separation keeps SQL-specific details from spreading throughout API endpoints and makes data-access strategies easier to evolve.


15. Use Dapper for Explicit SQL Where It Fits

Dapper can be a practical choice when an enterprise API needs direct control over SQL and lightweight object mapping.


var shipment =
    await connection.QuerySingleOrDefaultAsync<ShipmentSummary>(
        """
        SELECT
            ID,
            OrderID,
            TrackingNo,
            CourierID,
            SenderName,
            ReceiverName
        FROM tblOrders
        WHERE ID = @OrderId
        """,
        new
        {
            OrderId = shipmentId
        });
                

In a Razor view, the SQL parameter is written as @OrderId in the source file so the rendered article displays @OrderId correctly.


16. Isolate External API Integrations

Enterprise APIs frequently call external services. In shipping software, one application may integrate with DHL, UPS, DPD and Asendia. Keep those integrations behind a stable application contract.


public interface IShippingCarrier
{
    string Code { get; }

    Task<ShipmentResult> CreateShipmentAsync(
        ShipmentRequest request,
        CancellationToken cancellationToken);
}
                

The application workflow can resolve the selected carrier without embedding DHL API Integration, UPS API Integration, DPD API Integration or Asendia API Integration logic inside controllers.


17. Use HttpClientFactory for External Services

Configure named or typed HTTP clients for external integrations rather than constructing clients throughout controller code.


builder.Services.AddHttpClient<DhlShippingClient>(
    client =>
    {
        client.BaseAddress =
            new Uri(configuration["Dhl:BaseUrl"]!);

        client.Timeout =
            TimeSpan.FromSeconds(30);
    });
                

The integration component can then own authentication, serialization, endpoint details and response mapping.


18. Design Idempotent Operations for Retryable Workflows

Distributed systems can retry requests. A shipment creation endpoint therefore needs to consider what happens if a client submits the same logical operation more than once.

Use an application-level idempotency key or a business identifier where appropriate, and store enough state to recognize repeated submissions.


Request
   ↓
Idempotency Key
   ↓
Check Existing Operation
   ↓
Process Once
   ↓
Return Existing Result on Retry
                

This is particularly important when an API operation also creates an external shipment through a carrier service.


19. Version APIs Deliberately

Enterprise APIs can remain in production for years. Breaking changes should be planned rather than introduced accidentally.


/api/v1/shipments
/api/v2/shipments
                

API versioning can also be introduced through headers or other conventions. The important point is to establish a clear compatibility strategy for consumers.


20. Document APIs with OpenAPI

API documentation should describe endpoints, request schemas, response schemas, authentication requirements and common error conditions.


builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
                

Good OpenAPI documentation reduces integration friction for internal teams, customers and partner systems.


21. Add Structured Logging and Correlation

API logs become significantly more useful when they capture structured information rather than only plain text.


logger.LogInformation(
    "Shipment {ShipmentReference} processed by {CarrierCode}",
    shipment.Reference,
    carrier.Code);
                

Correlate API requests with shipment references, database operations and external carrier requests where practical. Avoid logging credentials and unnecessary customer data.


22. Implement Health and Readiness Checks

Production systems need a way to determine whether critical dependencies are available. Health checks can be used for the application itself and selected infrastructure dependencies.


builder.Services.AddHealthChecks()
    .AddSqlServer(
        configuration.GetConnectionString("Default"));
                

Keep readiness decisions practical. A temporary external carrier outage does not necessarily mean the entire API process must be considered unhealthy.


23. Protect APIs from Excessive Traffic

Rate limiting can protect authentication endpoints, search operations and expensive API calls from excessive traffic.

This is especially relevant for endpoints that trigger external carrier calls because every incoming request may translate into downstream network and service load.


24. Cache Stable Data, Not Everything

Some enterprise API responses change infrequently and can be cached where the business rules allow it.

Examples might include carrier service configuration, country lists and other reference data. Dynamic shipment status and carrier rates often require different freshness rules.


var services =
    await cache.GetOrCreateAsync(
        "carrier-services",
        async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow =
                TimeSpan.FromMinutes(30);

            return await repository
                .GetCarrierServicesAsync();
        });
                

25. Move Long-Running Work to Background Processing

An API request should not necessarily wait for a large synchronization or batch operation to finish. Tracking synchronization, bulk imports and scheduled processing can often run independently.


API Request
    ↓
Create Work Item
    ↓
Background Worker
    ↓
Process
    ↓
Persist Result
                

The appropriate implementation depends on reliability and delivery requirements. The important architectural point is to separate user-facing request latency from long-running processing.


26. Optimize API Performance at the Right Layer

Do not assume the ASP.NET Core framework is the bottleneck. Measure the complete request path.

  • API middleware and application processing
  • SQL Server query execution
  • Dapper or ORM mapping
  • Serialization
  • External carrier API latency
  • Network and infrastructure overhead

A slow shipment endpoint may actually be waiting on SQL Server or a DHL, UPS, DPD or Asendia request.


27. Test API Contracts and Business Workflows

Test both the HTTP contract and the underlying application behavior. Important scenarios include validation errors, authorization failures, missing resources, duplicate requests, carrier failures and successful processing.


var response =
    await client.PostAsJsonAsync(
        "/api/shipments",
        request,
        cancellationToken);

response.StatusCode
    .Should()
    .Be(HttpStatusCode.Created);
                

Integration tests can verify the behavior of the real API pipeline while unit tests can focus on application rules without invoking external carriers.


28. Applying REST API Best Practices to ShippingPortal

ShippingPortal is a useful example because its API layer needs to coordinate several business and infrastructure boundaries: shipments, orders, addresses, carrier services, tracking, SQL Server and external carrier APIs.


Client
  ↓
ASP.NET Core REST API
  ↓
Application Service
  ↓
Shipment Workflow
  ├── SQL Server / Dapper
  └── Carrier Integration
          ├── DHL
          ├── UPS
          ├── DPD
          └── Asendia
                

The API remains stable while the underlying data-access strategy and individual carrier implementations can evolve independently.


ASP.NET Core REST API Best Practices Checklist

  • Are resources and HTTP methods modeled consistently?
  • Are controllers thin and application workflows explicit?
  • Are API contracts separated from database entities?
  • Are request validation and authorization enforced at the right boundaries?
  • Are error responses consistent and useful?
  • Are large results paginated and filtered predictably?
  • Are asynchronous database and HTTP operations used appropriately?
  • Are external APIs isolated behind stable integration contracts?
  • Are retries and idempotency considered for non-trivial operations?
  • Are API versioning, logging, health checks and monitoring planned for production?

Conclusion

Good ASP.NET Core REST API design is about more than routing and JSON responses. Enterprise APIs need clear contracts, consistent HTTP behavior, strong validation, authentication and authorization, predictable performance and useful operational diagnostics.

The architecture becomes especially important in integration-heavy systems. A ShippingPortal-style application can expose shipment and tracking APIs while using SQL Server, Dapper and external carrier integrations behind those APIs. Keeping those boundaries clear reduces coupling and makes the application easier to evolve.

The strongest REST APIs are therefore not simply the ones with the most endpoints. They are the ones that remain predictable for consumers, maintainable for developers and observable for the teams operating them in production.


About the Author

I'm Kshitij Mehta, a Senior .NET Developer and ASP.NET Core Consultant with over 14 years of experience designing and developing enterprise software solutions.

My work includes C#, ASP.NET Core, SQL Server, Dapper, Entity Framework, REST APIs, logistics platforms, shipping software, carrier integrations and business applications.

This article focuses on practical ASP.NET Core REST API practices that can be applied to real enterprise systems, including integration-heavy platforms such as shipping applications.