DEV Community

Luqman Bolajoko
Luqman Bolajoko

Posted on

Best Practices for Building .NET Core Web APIs

When building APIs in .NET Core, ensuring they are efficient, secure, scalable, and easy to maintain is crucial. In this post, I’ll walk through some best practices for building .NET Core Web APIs, covering areas such as performance, security, and maintainability.


1. Leverage Dependency Injection

One of the biggest advantages of .NET Core is its built-in Dependency Injection (DI) support. DI helps in managing service lifetimes and reducing tight coupling between components. Use it to inject dependencies such as services, repositories, and other layers into your controllers.

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IMyService, MyService>();
}
Enter fullscreen mode Exit fullscreen mode

2. Follow RESTful Principles

Ensure your APIs follow standard RESTful conventions. Use proper HTTP methods (GET, POST, PUT, DELETE) for operations and return appropriate status codes (200 for success, 404 for not found, 201 for created, etc.).

[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
    var user = await _userService.GetUserAsync(id);
    return user == null ? NotFound() : Ok(user);
}
Enter fullscreen mode Exit fullscreen mode

3. Secure Your API

Security is critical. Always use HTTPS to encrypt communication between the client and server. Implement JWT (JSON Web Token) for authentication and authorization, allowing only authenticated users to access specific endpoints.

[Authorize]
[HttpGet("secure-data")]
public IActionResult GetSecureData()
{
    return Ok("This data is secured!");
}
Enter fullscreen mode Exit fullscreen mode

4. Centralize Error Handling

Use Middleware for centralized exception handling to ensure that errors are consistently handled across your API. This approach simplifies your controllers and keeps the error-handling logic separate.

public class ErrorHandlingMiddleware
{
    public async Task Invoke(HttpContext context, RequestDelegate next)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        context.Response.StatusCode = 500;
        return context.Response.WriteAsync(ex.Message);
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Use Data Transfer Objects (DTOs)

DTOs help manage the data flow between your client and server, ensuring that only required fields are exposed and minimizing data transfer size. They also prevent over-posting and under-posting attacks.

public class UserDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

6. API Versioning

Over time, your API will evolve, and breaking changes may occur. Use API versioning to maintain backward compatibility and ensure clients can continue using older versions without breaking.

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

7. Enable Caching for Better Performance

For APIs that return frequently requested data, implement caching to reduce server load and improve response times. You can use In-Memory Cache or Distributed Cache depending on your needs.

[ResponseCache(Duration = 60)]
[HttpGet("{id}")]
public IActionResult GetCachedUser(int id)
{
    var user = _userService.GetUser(id);
    return Ok(user);
}
Enter fullscreen mode Exit fullscreen mode

8. Log Everything

Logging is essential for monitoring and debugging APIs in production. Use libraries like Serilog or NLog to track requests, responses, and errors. Structured logging makes it easier to search and analyze logs.

public class MyService
{
    private readonly ILogger<MyService> _logger;

    public MyService(ILogger<MyService> logger)
    {
        _logger = logger;
    }

    public void Process()
    {
        _logger.LogInformation("Processing request at {Time}", DateTime.UtcNow);
    }
}
Enter fullscreen mode Exit fullscreen mode

9. Validate Incoming Data

Use Data Annotations or Fluent Validation to validate user input and ensure data integrity. Validation should occur at the API boundary to prevent invalid data from flowing through the system.

public class UserDto
{
    [Required]
    [StringLength(50, MinimumLength = 2)]
    public string Name { get; set; }

    [EmailAddress]
    public string Email { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

10. Implement Pagination and Filtering

For large datasets, implement pagination and filtering to avoid sending too much data in a single request. This approach improves performance and provides a better user experience.

[HttpGet]
public async Task<IActionResult> GetUsers([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10)
{
    var users = await _userService.GetUsersAsync(pageNumber, pageSize);
    return Ok(users);
}
Enter fullscreen mode Exit fullscreen mode

11. Use Async/Await for Non-Blocking Operations

To improve scalability and responsiveness, always use asynchronous methods for I/O-bound operations. This prevents blocking threads while waiting for external resources.

public async Task<IActionResult> GetUser(int id)
{
    var user = await _userService.GetUserByIdAsync(id);
    return Ok(user);
}
Enter fullscreen mode Exit fullscreen mode

12. Add Swagger for API Documentation

Use Swagger to generate interactive API documentation. It allows developers to easily explore your API’s endpoints, see request/response formats, and test the API in real-time.

public void ConfigureServices(IServiceCollection services)
{
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
    });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
    });
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

These best practices for .NET Core Web APIs ensure that your application is secure, performant, and scalable. Whether you’re building small APIs or enterprise-level systems, these tips will help make your codebase more maintainable and easier to work with.

Do you have other tips for improving .NET Core APIs? Drop them in the comments below!

Top comments (0)