| name | api-design |
| description | Design robust REST APIs with proper versioning, pagination, error handling, rate limiting, and OpenAPI documentation. |
API Design
Purpose: Design robust, maintainable, and user-friendly APIs.
RESTful Conventions
Resource Naming
ā
Good:
GET /api/v1/users # List users
POST /api/v1/users # Create user
GET /api/v1/users/{id} # Get specific user
PUT /api/v1/users/{id} # Update user (full)
PATCH /api/v1/users/{id} # Update user (partial)
DELETE /api/v1/users/{id} # Delete user
GET /api/v1/users/{id}/orders # Get user's orders (nested)
POST /api/v1/users/{id}/orders # Create order for user
ā Bad:
GET /api/v1/get_users
POST /api/v1/create_user
GET /api/v1/user_detail?id=123
HTTP Status Codes
200 OK
201 Created
204 No Content
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
409 Conflict
422 Unprocessable
429 Too Many Requests
500 Internal Server Error
503 Service Unavailable
Response Format
Success Response
{
"status": "success",
"data": {
"id": 123,
"email": "user@example.com",
"name": "John Doe"
},
"metadata": {
"timestamp": "2026-01-06T12:00:00Z",
"version": "1.0.0"
}
}
Error Response
{
"status": "error",
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{
"field": "email",
"message": "Invalid email format"
}
]
},
"metadata": {
"timestamp": "2026-01-06T12:00:00Z",
"request_id": "abc-123"
}
}
Response Models
public class ApiResponse<T>
{
public string Status { get; set; } = "success";
public T? Data { get; set; }
public ResponseMetadata Metadata { get; set; } = new();
}
public class ApiErrorResponse
{
public string Status { get; set; } = "error";
public ErrorDetails Error { get; set; } = new();
public ResponseMetadata Metadata { get; set; } = new();
}
public class ResponseMetadata
{
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public string Version { get; set; } = "1.0.0";
public string? RequestId { get; set; }
}
public class ErrorDetails
{
public string Code { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public List<ValidationError>? Details { get; set; }
}
public class ValidationError
{
public string Field { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
}
Pagination
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class PaginationRequest
{
public int Page { get; set; } = 1;
private int _perPage = 20;
public int PerPage
{
get => _perPage;
set => _perPage = Math.Min(value, 100);
}
}
public class PaginatedResponse<T>
{
public List<T> Data { get; set; } = new();
public PaginationMetadata Pagination { get; set; } = new();
}
public class PaginationMetadata
{
public int Page { get; set; }
public int PerPage { get; set; }
public int Total { get; set; }
public int Pages { get; set; }
public bool HasNext { get; set; }
public bool HasPrev { get; set; }
}
[ApiController]
[Route("api/v1/[controller]")]
public class UsersController : ControllerBase
{
private readonly ApplicationDbContext _context;
public UsersController(ApplicationDbContext context)
{
_context = context;
}
[HttpGet]
[ProducesResponseType(typeof(PaginatedResponse<UserDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> ListUsers([FromQuery] PaginationRequest request)
{
var query = _context.Users.AsQueryable();
var total = await query.CountAsync();
var pages = (int)Math.Ceiling(total / (double)request.PerPage);
var users = await query
.Skip((request.Page - 1) * request.PerPage)
.Take(request.PerPage)
.Select(u => new UserDto
{
Id = u.Id,
Email = u.Email,
Name = u.Name
})
.ToListAsync();
var response = new PaginatedResponse<UserDto>
{
Data = users,
Pagination = new PaginationMetadata
{
Page = request.Page,
PerPage = request.PerPage,
Total = total,
Pages = pages,
HasNext = request.Page < pages,
HasPrev = request.Page > 1
}
};
return Ok(response);
}
}
Rate Limiting
using AspNetCoreRateLimit;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMemoryCache();
builder.Services.Configure<IpRateLimitOptions>(options =>
{
options.GeneralRules = new List<RateLimitRule>
{
new RateLimitRule
{
Endpoint = "*",
Period = "1m",
Limit = 60
},
new RateLimitRule
{
Endpoint = "*/api/search",
Period = "1m",
Limit = 10
}
};
});
builder.Services.AddInMemoryRateLimiting();
builder.Services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
var app = builder.Build();
app.UseIpRateLimiting();
app.MapControllers();
app.Run();
}
}
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.User.Identity?.Name ?? context.Request.Headers.Host.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 10,
Window = TimeSpan.FromMinutes(1)
}));
});
[ApiController]
[Route("api/v1/[controller]")]
public class SearchController : ControllerBase
{
[HttpGet]
[EnableRateLimiting("fixed")]
public async Task<IActionResult> Search([FromQuery] string query)
{
return Ok(results);
}
}
Versioning
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Versioning;
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
});
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class UsersV1Controller : ControllerBase
{
[HttpGet]
public IActionResult GetUsers()
{
return Ok(new { version = "1.0", users = new[] { "user1", "user2" } });
}
}
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class UsersV2Controller : ControllerBase
{
[HttpGet]
public IActionResult GetUsers()
{
return Ok(new
{
version = "2.0",
users = new[]
{
new { id = 1, name = "user1" },
new { id = 2, name = "user2" }
}
});
}
}
[ApiController]
[ApiVersion("1.0")]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public IActionResult GetV1()
{
return Ok(new { version = "1.0" });
}
[HttpGet]
[MapToApiVersion("2.0")]
public IActionResult GetV2()
{
return Ok(new { version = "2.0" });
}
}
Request Validation
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
public class CreateUserRequest
{
[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Invalid email format")]
public string Email { get; set; } = string.Empty;
[Required(ErrorMessage = "Name is required")]
[StringLength(100, MinimumLength = 2, ErrorMessage = "Name must be between 2 and 100 characters")]
public string Name { get; set; } = string.Empty;
[Required(ErrorMessage = "Password is required")]
[StringLength(100, MinimumLength = 8, ErrorMessage = "Password must be at least 8 characters")]
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]",
ErrorMessage = "Password must contain uppercase, lowercase, number, and special character")]
public string Password { get; set; } = string.Empty;
[Range(18, 120, ErrorMessage = "Age must be between 18 and 120")]
public int Age { get; set; }
}
[ApiController]
[Route("api/v1/[controller]")]
public class UsersController : ControllerBase
{
[HttpPost]
[ProducesResponseType(typeof(UserDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest request)
{
if (!ModelState.IsValid)
{
var errors = ModelState
.Where(x => x.Value?.Errors.Count > 0)
.SelectMany(x => x.Value!.Errors.Select(e => new ValidationError
{
Field = x.Key,
Message = e.ErrorMessage
}))
.ToList();
return UnprocessableEntity(new ApiErrorResponse
{
Error = new ErrorDetails
{
Code = "VALIDATION_ERROR",
Message = "Invalid input data",
Details = errors
}
});
}
var user = await _userService.CreateUserAsync(request);
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}
}
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
var errors = context.ModelState
.Where(x => x.Value?.Errors.Count > 0)
.SelectMany(x => x.Value!.Errors.Select(e => new ValidationError
{
Field = x.Key,
Message = e.ErrorMessage
}))
.ToList();
context.Result = new UnprocessableEntityObjectResult(new ApiErrorResponse
{
Error = new ErrorDetails
{
Code = "VALIDATION_ERROR",
Message = "Invalid input data",
Details = errors
}
});
}
}
}
builder.Services.AddControllers(options =>
{
options.Filters.Add<ValidateModelAttribute>();
});
API Documentation with Swagger
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.Annotations;
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "My API",
Version = "v1",
Description = "API for managing users and products",
Contact = new OpenApiContact
{
Name = "Support Team",
Email = "support@example.com"
}
});
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
options.RoutePrefix = string.Empty;
});
}
[ApiController]
[Route("api/v1/[controller]")]
public class UsersController : ControllerBase
{
[HttpGet("{id}")]
[ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetUser(int id)
{
var user = await _userService.GetUserAsync(id);
if (user == null)
return NotFound();
return Ok(user);
}
}
Related Skills: