🌐

REST API Design Best Practices

APIs & Integration Intermediate 2 min read 400 words
REST API

REST API Design Best Practices

A comprehensive guide to designing RESTful APIs following industry best practices.

Resource Naming Conventions

Use Nouns, Not Verbs

βœ… Good:
GET /users
GET /users/123
POST /users
PUT /users/123
DELETE /users/123

❌ Bad:
GET /getUsers
POST /createUser
PUT /updateUser/123
DELETE /deleteUser/123

Use Plural Nouns

βœ… Good:
GET /users
GET /orders
GET /products

❌ Bad:
GET /user
GET /order
GET /product

Use Kebab-Case for Multi-Word Resources

βœ… Good:
GET /user-accounts
GET /order-items
GET /shipping-addresses

❌ Bad:
GET /userAccounts
GET /user_accounts

HTTP Methods

Method Description Idempotent Safe
GET Retrieve resource(s) Yes Yes
POST Create resource No No
PUT Replace entire resource Yes No
PATCH Partial update No No
DELETE Remove resource Yes No

ASP.NET Core Implementation

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    // GET api/users
    [HttpGet]
    public async Task<ActionResult<IEnumerable<UserDto>>> GetAll()
    {
        var users = await _userService.GetAllAsync();
        return Ok(users);
    }

    // GET api/users/5
    [HttpGet("{id}")]
    public async Task<ActionResult<UserDto>> GetById(int id)
    {
        var user = await _userService.GetByIdAsync(id);
        if (user == null) return NotFound();
        return Ok(user);
    }

    // POST api/users
    [HttpPost]
    public async Task<ActionResult<UserDto>> Create(CreateUserDto dto)
    {
        var user = await _userService.CreateAsync(dto);
        return CreatedAtAction(nameof(GetById), new { id = user.Id }, user);
    }

    // PUT api/users/5
    [HttpPut("{id}")]
    public async Task<IActionResult> Update(int id, UpdateUserDto dto)
    {
        if (id != dto.Id) return BadRequest();
        await _userService.UpdateAsync(dto);
        return NoContent();
    }

    // DELETE api/users/5
    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(int id)
    {
        await _userService.DeleteAsync(id);
        return NoContent();
    }
}

HTTP Status Codes

Success Codes (2xx)

Code Name Use Case
200 OK Successful GET, PUT, PATCH
201 Created Successful POST creating resource
204 No Content Successful DELETE or PUT with no body

Client Error Codes (4xx)

Code Name Use Case
400 Bad Request Validation error, malformed request
401 Unauthorized Missing or invalid authentication
403 Forbidden Valid auth but insufficient permissions
404 Not Found Resource doesn’t exist
409 Conflict Resource state conflict
422 Unprocessable Entity Semantic validation error

Server Error Codes (5xx)

Code Name Use Case
500 Internal Server Error Unexpected server error
503 Service Unavailable Server temporarily unavailable

Filtering, Sorting & Pagination

Query Parameters

GET /users?status=active&role=admin        # Filtering
GET /users?sort=created_at&order=desc      # Sorting
GET /users?page=2&limit=20                 # Pagination
GET /users?fields=id,name,email            # Field selection

Implementation

public class QueryParameters
{
    private const int MaxPageSize = 100;
    private int _pageSize = 10;

    public int Page { get; set; } = 1;
    public int PageSize
    {
        get => _pageSize;
        set => _pageSize = Math.Min(value, MaxPageSize);
    }
    public string? SortBy { get; set; }
    public bool Descending { get; set; }
    public string? Status { get; set; }
}

[HttpGet]
public async Task<ActionResult<PagedResult<UserDto>>> GetAll(
    [FromQuery] QueryParameters parameters)
{
    var result = await _userService.GetPagedAsync(parameters);

    Response.Headers.Add("X-Total-Count", result.TotalCount.ToString());
    Response.Headers.Add("X-Page", parameters.Page.ToString());
    Response.Headers.Add("X-Page-Size", parameters.PageSize.ToString());

    return Ok(result.Items);
}

API Versioning

URI Versioning

GET /api/v1/users
GET /api/v2/users

Header Versioning

GET /api/users
X-API-Version: 2

Query Parameter Versioning

GET /api/users?api-version=2.0

ASP.NET Core Setup

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new HeaderApiVersionReader("X-API-Version"),
        new QueryStringApiVersionReader("api-version")
    );
});

[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class UsersController : ControllerBase
{
    [HttpGet]
    [MapToApiVersion("1.0")]
    public IActionResult GetV1() => Ok("Version 1.0");

    [HttpGet]
    [MapToApiVersion("2.0")]
    public IActionResult GetV2() => Ok("Version 2.0");
}

Error Response Format

Standard Error Response

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "Bad Request",
    "status": 400,
    "traceId": "00-abc123...",
    "errors": {
        "Email": ["The Email field is required."],
        "Password": ["Password must be at least 8 characters."]
    }
}

Custom Error Response

public class ApiError
{
    public int StatusCode { get; set; }
    public string Message { get; set; }
    public string? Detail { get; set; }
    public string? TraceId { get; set; }
    public Dictionary<string, string[]>? Errors { get; set; }
}

// Usage
return BadRequest(new ApiError
{
    StatusCode = 400,
    Message = "Validation failed",
    Errors = new Dictionary<string, string[]>
    {
        ["Email"] = new[] { "Invalid email format" }
    }
});

HATEOAS (Hypermedia)

{
    "id": 123,
    "name": "John Doe",
    "_links": {
        "self": { "href": "/api/users/123" },
        "orders": { "href": "/api/users/123/orders" },
        "edit": { "href": "/api/users/123", "method": "PUT" },
        "delete": { "href": "/api/users/123", "method": "DELETE" }
    }
}

Rate Limiting

builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("fixed", config =>
    {
        config.PermitLimit = 100;
        config.Window = TimeSpan.FromMinutes(1);
        config.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        config.QueueLimit = 10;
    });
});

app.UseRateLimiter();

[EnableRateLimiting("fixed")]
public class UsersController : ControllerBase { }

Best Practices Summary

  1. Use consistent naming - kebab-case, plural nouns
  2. Return appropriate status codes - 201 for created, 204 for no content
  3. Version your API - Plan for breaking changes
  4. Implement pagination - Prevent large data transfers
  5. Use standard error format - Consistent error responses
  6. Document with OpenAPI/Swagger - Self-documenting APIs
  7. Implement rate limiting - Protect against abuse
  8. Use HTTPS only - Secure all communications

Sources

  • APIs/REST API DESIGN.png

πŸ“š Related Articles