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:

Example.cs
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:

data.json
{
  "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.

Program.cs
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:

Example.cs
// URL path versioning — simple and explicit
app.MapGet("/api/v1/orders/{id}", HandleGetOrderV1);
app.MapGet("/api/v2/orders/{id}", HandleGetOrderV2);
Example.cs
// 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:

Example.cs
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:

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.