REST API Design Mistakes to Avoid (Practical Tips) .NET 9.0

This document outlines common REST API design mistakes in ASP.NET Core (.NET 9.0) and provides practical tips to avoid them, ensuring scalable, maintainable, and secure APIs. It leverages features introduced or enhanced in .NET 9.0, such as improved minimal APIs, better OpenAPI support, enhanced performance, and native AOT compatibility.

1. Using Incorrect HTTP Methods

Mistake : Treating all operations as POST requests, ignoring semantic HTTP methods.

Why it's bad : Violates REST principles, confuses clients, and hinders caching/interoperability.

Tip : Use appropriate HTTP verbs: GET (read), POST (create), PUT (update), PATCH (partial update), DELETE (remove).

Bad Example

  
    [HttpPost("getUser")]
public IActionResult GetUser(int id)
{
    var user = _userService.GetById(id);
    return Ok(user);
}
  

Good Example

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

2. Poor Resource Naming Conventions

Mistake : Using verbs in URIs like /getUsers or /createUser.

Why it's bad : URIs should represent resources, not actions. Leads to inconsistent and hard-to-maintain APIs.

Tip : Use nouns for resources, keep URIs simple and hierarchical. Use query parameters for filtering.

Bad Example

  
    GET /getAllUsers
POST /createNewUser
  

Good Example

  
    GET /api/users
GET /api/users?status=active
POST /api/users
  

3. Ignoring HTTP Status Codes

Mistake : Always returning 200 OK, even for errors or not found resources.

Why it's bad : Clients can't distinguish success from failure, leading to poor error handling.

Tip : Return appropriate status codes (200, 201, 400, 401, 404, 500, etc.) and use ProblemDetails for errors.

Bad Example

  
    [HttpGet("{id}")]
public IActionResult GetUser(int id)
{
    var user = _userService.GetById(id);
    return Ok(new { success = user != null, data = user });
}
  

Good Example

  
    [HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
    var user = await _userService.GetByIdAsync(id);
    if (user == null)
        return NotFound(new ProblemDetails { Title = "User not found" });
    return Ok(user);
}
  

4. Not Versioning Your API

Mistake : No versioning, breaking changes affect all clients immediately.

Why it's bad : Backward compatibility issues, forces all clients to update simultaneously.

Tip : Use URL versioning (e.g., /api/v1/users) or header-based versioning. Plan for deprecation.

Example

  
    [ApiController]
[Route("api/v{version:apiVersion}/users")]
[ApiVersion("1.0")]
public class UsersController : ControllerBase
{
    // Controller methods
}
  

5. Over-fetching or Under-fetching Data

Mistake : Returning all fields always or too few, forcing clients to make multiple requests.

Why it's bad : Inefficient network usage, poor performance.

Tip : Use DTOs (Data Transfer Objects) and allow field selection via query parameters.

Example

  
    [HttpGet]
public async Task<IActionResult> GetUsers([FromQuery] string fields = null)
{
    var users = await _userService.GetAllAsync();
    if (!string.IsNullOrEmpty(fields))
    {
        var selectedFields = fields.Split(',');
        // Apply field selection logic
    }
    return Ok(users);
}
  

6. Synchronous Operations in Controllers

Mistake : Using synchronous methods in ASP.NET Core controllers.

Why it's bad : Blocks threads, reduces scalability under load.

Tip : Always use async/await for I/O operations.

Bad Example

  
    [HttpGet]
public IActionResult GetUsers()
{
    var users = _userService.GetAll(); // Synchronous
    return Ok(users);
}
  

Good Example

  
    [HttpGet]
public async Task<IActionResult> GetUsers()
{
    var users = await _userService.GetAllAsync();
    return Ok(users);
}
  

7. Exposing Internal Exceptions

Mistake : Letting framework exceptions bubble up to clients.

Why it's bad : Security risk (information disclosure), poor user experience.

Tip : Use global exception handling with custom error responses.

Example  (in Startup.cs or Program.cs)

  
    builder.Services.AddControllers(options =>
{
    options.Filters.Add(new GlobalExceptionFilter());
});
  

8. Lack of Input Validation

Mistake : No validation on incoming data, trusting client input blindly.

Why it's bad : Security vulnerabilities, data corruption.

Tip : Use data annotations and ModelState validation.

Example

  
    public class CreateUserRequest
{
    [Required]
    [StringLength(50)]
    public string Name { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }
}

[HttpPost]
public async Task<IActionResult> CreateUser(CreateUserRequest request)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);

    // Create user logic
    return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}
  

9. Not Using Proper Content Negotiation

Mistake : Assuming JSON only, ignoring Accept headers.

Why it's bad : Limits API flexibility, poor client experience.

Tip : Support multiple formats (JSON, XML) and respect Accept/Content-Type headers.

Example : ASP.NET Core handles this automatically, but ensure proper formatters are configured.

10. Ignoring Caching Headers

Mistake : No caching directives, causing unnecessary server load.

Why it's bad : Poor performance, increased costs.

Tip : Use appropriate cache headers for GET requests.

Example

  
    [HttpGet]
[ResponseCache(Duration = 300)] // Cache for 5 minutes
public async Task<IActionResult> GetUsers()
{
    var users = await _userService.GetAllAsync();
    return Ok(users);
}
  

11. Tight Coupling Between Controllers and Business Logic

Mistake : Business logic directly in controllers.

Why it's bad : Hard to test, maintain, and reuse.

Tip : Use service layer pattern, dependency injection.

Example

public class UsersController : ControllerBase
{
    private readonly IUserService _userService;

    public UsersController(IUserService userService)
    {
        _userService = userService;
    }

    [HttpGet]
    public async Task<IActionResult> GetUsers()
    {
        return Ok(await _userService.GetAllAsync());
    }
}

12. Not Documenting Your API

Mistake: No API documentation, leaving developers guessing.

Why it's bad: Poor developer experience, integration difficulties.

Tip: Use enhanced OpenAPI support in .NET 9.0 for automatic documentation with better type information and examples.

Example (in Program.cs)

builder.Services.AddOpenApi(); // Enhanced in .NET 9.0
app.MapOpenApi();
app.UseSwaggerUI(options =>
{
    options.SwaggerEndpoint("/openapi/v1.json", "v1");
});

13. Inconsistent Error Response Format

Mistake: Different error formats across endpoints.

Why it's bad: Confuses clients, harder error handling.

Tip: Standardize error responses using ProblemDetails.

Example

return BadRequest(new ProblemDetails
{
    Title = "Validation Error",
    Detail = "The request contains invalid data",
    Status = 400
});

14. Not Implementing Rate Limiting

Mistake: No protection against abuse or DoS attacks.

Why it's bad: Security risk, resource exhaustion.

Tip: Use the improved rate limiting middleware in .NET 9.0 with better performance and additional limiter types.

Example

builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("fixed", opt =>
    {
        opt.Window = TimeSpan.FromMinutes(1);
        opt.PermitLimit = 100;
    });
    options.AddSlidingWindowLimiter("sliding", opt => // New in .NET 9.0
    {
        opt.Window = TimeSpan.FromMinutes(1);
        opt.SegmentsPerWindow = 4;
        opt.PermitLimit = 100;
    });
});

15. Ignoring Security Best Practices

Mistake: No authentication/authorization, plain text secrets.

Why it's bad: Data breaches, unauthorized access.

Tip: Use JWT, OAuth, HTTPS always, validate inputs.

Example

[Authorize]
[HttpGet]
public async Task<IActionResult> GetUserProfile()
{
    var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    // Get user profile logic
}

16. Not Leveraging Minimal APIs (New in .NET 9.0)

Mistake: Still using full controllers for simple endpoints, adding unnecessary overhead.

Why it's bad: Over-engineering simple APIs, reduced performance.

Tip: Use minimal APIs for simple endpoints in .NET 9.0, which now support more advanced features like filters and better performance.

Example

app.MapGet("/api/users/{id}", async (int id, IUserService userService) =>
{
    var user = await userService.GetByIdAsync(id);
    return user is not null ? Results.Ok(user) : Results.NotFound();
})
.WithName("GetUser")
.WithOpenApi();

17. Ignoring Native AOT Compatibility

Mistake: Writing code that prevents ahead-of-time compilation.

Why it's bad: Slower startup times, larger memory footprint in constrained environments.

Tip: Ensure your APIs are Native AOT compatible in .NET 9.0 for better performance in serverless or containerized environments.

Example: Avoid reflection-heavy code; use source generators where possible.

Conclusion

Avoiding these mistakes leads to robust, scalable, and maintainable REST APIs in .NET 9.0. Leverage new features like enhanced minimal APIs, improved OpenAPI support, and Native AOT compatibility. Always consider security, performance, and developer experience. Test your APIs thoroughly and gather feedback from consumers.

Happy Coding!