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.
- 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.