REST API Design Conventions That Actually Matter
REST API design is full of opinions. Some conventions improve developer experience measurably; others are cargo-culted from blog posts written a decade ago. This article focuses on the conventions that actually reduce confusion and support for .NET API teams, with practical examples using ASP.NET Core.
Resource Naming
Use plural nouns for collection endpoints. Use lowercase, hyphenated paths. Avoid verbs in URLs — the HTTP method is the verb.
GET /api/orders — list orders
POST /api/orders — create an order
GET /api/orders/42 — get order 42
PUT /api/orders/42 — replace order 42
PATCH /api/orders/42 — partially update order 42
DELETE /api/orders/42 — delete order 42
For sub-resources, nest them under the parent:
GET /api/orders/42/line-items
POST /api/orders/42/line-items
Avoid deep nesting beyond two levels. If you find yourself writing /api/customers/5/orders/42/line-items/3/notes, consider promoting the resource to a top-level endpoint with a query filter instead.
HTTP Methods and Status Codes
Use methods and status codes as the HTTP specification intends:
app.MapGet("/api/orders/{id:int}", async (int id, AppDbContext db) =>
{
var order = await db.Orders.FindAsync(id);
return order is not null
? Results.Ok(order) // 200
: Results.NotFound(); // 404
});
app.MapPost("/api/orders", async (CreateOrderRequest request, AppDbContext db) =>
{
var order = new Order { /* map from request */ };
db.Orders.Add(order);
await db.SaveChangesAsync();
return Results.Created($"/api/orders/{order.Id}", order); // 201
});
app.MapDelete("/api/orders/{id:int}", async (int id, AppDbContext db) =>
{
var order = await db.Orders.FindAsync(id);
if (order is null) return Results.NotFound();
db.Orders.Remove(order);
await db.SaveChangesAsync();
return Results.NoContent(); // 204
});
A quick reference for the most common status codes:
| Code | Meaning | When to use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST that creates a resource |
| 204 | No Content | Successful DELETE or update with no response body |
| 400 | Bad Request | Validation failure or malformed input |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Business rule violation or concurrency conflict |
| 422 | Unprocessable Entity | Semantically invalid request |
Consistent Response Envelopes
Do not wrap successful responses in an envelope. Return the resource directly:
{
"id": 42,
"customerName": "Acme Ltd",
"total": 199.99,
"status": "confirmed"
}
Wrapping every response in { "data": ..., "success": true } adds noise and duplicates what HTTP status codes already communicate. Reserve structured wrappers for error responses using ProblemDetails (covered in a separate article).
Naming Conventions in JSON
Use camelCase for JSON properties. ASP.NET Core does this by default with System.Text.Json. Be consistent — mixing snake_case and camelCase across endpoints confuses consumers.
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});
Omitting null properties by default keeps responses lean without requiring clients to handle absent versus null fields.
Versioning
Version your API when you make breaking changes. The two practical approaches in .NET are URL path versioning and header versioning:
// URL path versioning — simple and explicit
app.MapGet("/api/v1/orders/{id}", HandleGetOrderV1);
app.MapGet("/api/v2/orders/{id}", HandleGetOrderV2);
// Header versioning with Asp.Versioning
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
});
URL path versioning is easier to understand, test, and cache. Header versioning keeps URLs clean but requires documentation and tooling awareness. Pick one and use it consistently.
Bulk Operations
When clients need to create or update multiple resources, provide a bulk endpoint rather than forcing N individual requests:
app.MapPost("/api/orders/batch", async (List<CreateOrderRequest> requests, AppDbContext db) =>
{
var orders = requests.Select(r => new Order { /* map */ }).ToList();
db.Orders.AddRange(orders);
await db.SaveChangesAsync();
return Results.Ok(orders.Select(o => new { o.Id, o.Status }));
});
Return individual statuses for each item in the batch so clients know which succeeded and which failed.
Practical Defaults
A few more conventions that save time:
- Always return
Locationheaders on201 Createdresponses. - Support
?fields=id,namefor large resources where clients only need a subset. - Use ISO 8601 for dates (
2026-01-15T09:00:00Z).System.Text.Jsondoes this by default. - Document everything — even well-designed APIs are frustrating without documentation.
These conventions are not rules etched in stone, but following them consistently across your API surface reduces cognitive load for every developer who consumes your services.