RESTful API Best Practices for .NET Developers
Complete guide to RESTful API architecture and best practices for .NET developers
By Ajith joseph · · Updated · 8 min read · intermediate
RESTful API Best Practices for .NET Developers
A complete guide to designing and implementing RESTful APIs using .NET, focusing on architecture, best practices, and practical examples.
Introduction to RESTful Architecture
REST (Representational State Transfer) is an architectural style for designing networked applications. It relies on stateless, client-server communication, typically over HTTP, and follows these key principles:
- Stateless: Each request contains all necessary information; no client state is stored on the server.
- Client-Server: Separation of concerns between UI and data storage.
- Uniform Interface: Consistent API design using resources, HTTP methods, and status codes.
- Cacheable: Responses can be cached to improve performance.
- Layered System: APIs can be composed of multiple layers (e.g., load balancers, gateways).
Project Setup in .NET
Create a new ASP.NET Core Web API project:
(dotnet new webapi -n MyRestApi --use-minimal-apis
cd MyRestApi
dotnet run)
Resource Design and Endpoints
Naming Conventions
- Use nouns for resources (e.g., /users, not /getUsers).
- Use plural forms (e.g., /products, not /product).
- Use hyphens for readability (e.g., /order-details, not /orderDetails).
HTTP Methods
// Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/api/products", async (ProductDbContext db) =>
await db.Products.ToListAsync())
.WithName("GetProducts");
app.MapGet("/api/products/{id}", async (int id, ProductDbContext db) =>
await db.Products.FindAsync(id) is Product product
? Results.Ok(product)
: Results.NotFound())
.WithName("GetProduct");
app.MapPost("/api/products", async (Product product, ProductDbContext db) =>
{
db.Products.Add(product);
await db.SaveChangesAsync();
return Results.Created(quot;/api/products/{product.Id}", product);
}).WithName("CreateProduct");
app.Run();
Status Codes and Responses
- 200 OK: Successful GET request.
- 201 Created: Resource successfully created (POST).
- 204 No Content: Successful update/delete with no response body.
- 400 Bad Request: Invalid input.
- 404 Not Found: Resource not found.
- 500 Internal Server Error: Server-side error.
Example with error handling:
app.MapPut("/api/products/{id}", async (int id, Product input, ProductDbContext db) =>
{
var product = await db.Products.FindAsync(id);
if (product is null) return Results.NotFound();
product.Name = input.Name;
product.Price = input.Price;
await db.SaveChangesAsync();
return Results.NoContent();
}).Produces(204).ProducesProblem(404);
Versioning
Implement API versioning to maintain backward compatibility:
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
});
// Usage: /api/v1/products
Data Validation
Use data annotations and FluentValidation:
public class Product
{
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; }
[Range(0.01, 10000)]
public decimal Price { get; set; }
}
// In Program.cs
builder.Services.AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Program>());
Security Best Practices
Use HTTPS only. Implement authentication (e.g., JWT):
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
});
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/api/products", async (ProductDbContext db) =>
await db.Products.ToListAsync())
.RequireAuthorization();
Performance Optimization
Use response caching:
app.MapGet("/api/products", async (ProductDbContext db) =>
await db.Products.ToListAsync())
.CacheOutput(c => c.Expire(TimeSpan.FromMinutes(5)));
Implement pagination:
public record PaginationParams(int PageNumber = 1, int PageSize = 10);
app.MapGet("/api/products", async (ProductDbContext db, [AsParameters] PaginationParams p) =>
await db.Products
.Skip((p.PageNumber - 1) * p.PageSize)
.Take(p.PageSize)
.ToListAsync());
Documentation with Swagger
Add Swagger for API documentation:
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
app.UseSwagger();
app.UseSwaggerUI();
Error Handling
Global exception handling:
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null)
{
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new
{
error = "An unexpected error occurred",
detail = error.Error.Message
});
}
});
});
Testing
Write unit tests using xUnit:
public class ProductTests
{
[Fact]
public async Task GetProducts_ReturnsProducts()
{
var dbContext = new Mock<ProductDbContext>();
var products = new List<Product> { new() { Id = 1, Name = "Test", Price = 10m } };
dbContext.Setup(x => x.Products.ToListAsync(default)).ReturnsAsync(products);
var result = await Program.GetProducts(dbContext.Object);
Assert.Equal(products, result.Value);
}
}
This guide covers the essentials of building RESTful APIs in .NET with best practices for scalability, security, and maintainability.