Web API Mistakes That Could Ruin Your Project (And How to Avoid Them)

Web API Mistakes That Could Ruin Your Project (And How to Avoid Them)

Building a robust and efficient Web API is essential for any application, but it’s easy to make mistakes that can cause serious issues down the line. From incorrect HTTP methods to flawed error handling, these mistakes can create confusion, hinder performance, and even ruin your project. In this post, we’ll dive into the most common Web API mistakes developers make and, more importantly, how to avoid them.

Whether you’re just starting or looking to refine your API practices, this guide will help you avoid critical pitfalls and ensure a smoother development process. Let’s dive in!

 

1 – Using Incorrect HTTP Methods and Status Codes

This could ruin your API: Using incorrect HTTP methods and status codes can confuse clients and break the consistency of your API, making it harder to maintain and integrate with other services.

C#
				// Using GET for creating a user, which is incorrect as GET should only retrieve data
[HttpGet("/api/users")]
public async Task<IActionResult> CreateUserAsync([FromBody] CreateUserRequest user)
{
    // User creation logic
    return Ok(user);  // Status code 200 is incorrect for creation; should be 201
}

// Using POST to get a user, which is semantically wrong as POST should be for creation
[HttpPost("/api/users/{id}")]
public async Task<IActionResult> GetUserAsync(int id)
{
    var user = await _userService.GetByIdAsync(id);
    return Ok(user);  // Status code 200 is fine for a GET, but POST is used incorrectly
}
			

Here’s how to avoid it: By using the correct HTTP methods and status codes, you ensure that your API is RESTful, consistent, and easy to integrate with. Here’s the corrected version:

C#
				// Correct method for retrieving data: GET
[HttpGet("/api/users/{id}")]
public async Task<IActionResult> GetUserAsync(int id)
{
    var user = await _userService.GetByIdAsync(id);
    if (user == null)
    {
        return NotFound();  // Correct status code: 404
    }
    return Ok(user); // Correct status code: 200
}
			

 

2 – Not Having Strongly Typed API Responses

This could ruin your API: Returning untyped or loosely structured responses can lead to confusion for both the API consumers and developers. Without clear models, the response data might not be predictable, making it difficult to handle and integrate with other systems.

C#
				[HttpGet("/api/users/{id}")]
public async Task<IActionResult> GetUserAsync(int id)
{
    var user = await _userService.GetByIdAsync(id);
    
    // Returning a loose, untyped response without any clear model
    return Ok(new {
        user.Id,
        user.Name,
        user.Email,
        user.Address,
        // Returning data without a clear response model
        Status = "Success",
        Timestamp = DateTime.Now
    });
}
			

Here’s how to avoid it: By using a strongly typed response model, you ensure that both the API and client are aligned on the structure of the data, making it easier to manage and reducing the chance for bugs.

C#
				[HttpGet("/api/users/{id}")]
public async Task<IActionResult> GetUserAsync(int id)
{
    var user = await _userService.GetByIdAsync(id);
    if (user == null)
    {
        return NotFound();
    }
    
    // Returning a strongly typed response model
    // Here you can also create a method within your class to do manual mapping
    var userResponse = new UserResponse
    {
        Id = user.Id,
        Name = user.Name,
        Email = user.Email,
        Address = user.Address
    };
    
    return Ok(userResponse);
}
			

You can further improve this top 1 and 2 by implementing the Result Pattern, which provides a more structured and reusable approach to handling success and failure  scenarios in your API, I also usually bring it already mapped from the service.

 

3 – Adding Verbs to Endpoints or Poorly Written Ones

This could ruin your API: Poorly named endpoints, especially those that include unnecessary HTTP verbs, can make your API harder to use and less intuitive. Endpoints should be clear, consistent, and follow RESTful conventions.

C#
				[HttpGet("/api/users/GetUserById/{id}")]
public async Task<IActionResult> GetUserById(int id)
{
    //...
}

[HttpDelete("executeDeleteUser/{id}")]
public async Task<IActionResult> ExecuteDeleteUser(int id)
{
    //...
}
			

   – The HTTP method already implies the action, so adding verbs like CreateUser, GetUserById, and DeleteUser is redundant.

   – The endpoint names are unnecessarily long and inconsistent.

Here’s how to avoid it: Stick to clean, resource-based naming conventions, allowing HTTP methods to define the action.

C#
				[HttpGet("/api/users/{id}")]
public async Task<IActionResult> GetUserAsync(int id)
{
    //...
}

[HttpDelete("/api/users/{id}")]
public async Task<IActionResult> DeleteUserAsync(int id)
{
    //...
}
			

 

4 -Neglecting Proper Error Handling

This could ruin your API: Poor error handling can make debugging a nightmare and lead to unpredictable behavior for API consumers. Returning generic error messages, exposing sensitive information, or failing to catch exceptions can create security risks and degrade the user experience.

C#
				if (request.GoogleAccountId < 0)
{
    return BadRequest("Google Account ID is invalid");
}
			

Here’s how to avoid it: Use a structured error response (ApiErrorReponse), return appropriate status codes, and ensure meaningful messages.

C#
				if (request.GoogleAccountId < 0)
{
    return UnprocessableEntity(new ApiErrorResponse
    {
        Status = ErrorCode.InvalidGoogleAccountId,
        Title = "Invalid Google Account ID",
        Message = ErrorMessages.InvalidGoogleAccountId
    });
}

var user = await _authService.LoginWithGoogleAsync(request.GoogleAccountId);
if (user == null)
{
    return NotFound(new ApiErrorResponse
    {
        Status = ErrorCode.NotFound,
        Title = "User Not Found",
        Detail = "No user found with the provided Google Account ID."
    });
}
			

You can also use try-catch to catch generic exceptions or create custom exceptions for better control over your API errors.

C#
				try
{
    //...
}
catch (InvalidGoogleAccountIdException ex)
{
    //Custom exception
    _logger.LogError($"Invalid Google Account ID: {ex.Message}");
    return BadRequest(new ApiErrorResponse
    {
        Status = ErrorCode.InvalidGoogleAccountId,
        Title = "Invalid Google Account ID",
        Detail = ex.Message
    });
}
catch (Exception ex)
{
    //Generic exception
    //LogError
    //return meaningful message
}
			

 

5 – Missing Authentication and Authorization

This could ruin your API: Neglecting proper authentication and authorization opens the door to unauthorized access, exposing sensitive data and allowing potential malicious actions. Without authentication, anyone can access your API, and without proper authorization, they can perform actions they shouldn’t, potentially leading to data breaches and compromised system integrity.

C#
				// API endpoint with no authentication
[HttpGet("/api/users/{id}")]
public async Task<IActionResult> GetUserAsync(int id)
{
    var user = await _userService.GetByIdAsync(id); 
    return Ok(user);  // No authentication, any user can access this
}
			

Here’s how to avoid it: Always implement authentication and authorization in your API to protect sensitive data and ensure that only authorized users can perform certain actions. Authentication ensures that the user is who they say they are, and authorization ensures they have permission to perform the requested operation.

C#
				services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = "yourIssuer",
        ValidAudience = "yourAudience",
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("yourSecretKey"))
    };
});


app.UseAuthentication();
app.UseAuthorization();
			

Now, to protect your endpoints with authentication, simply use the [Authorize] attribute on the controllers or methods you want to restrict, you can also apply roles:

C#
				[HttpGet("/api/users/{id}")]
[Authorize]
public async Task<IActionResult> GetUserAsync(int id)
{
    var user = await _userService.GetByIdAsync(id); 
    return Ok(user);
}
			

 

6 – Poor Pagination Handling, Not Optimizing Database, and Not Using EF Correctly

This could ruin your API: Poor pagination handling, inefficient database queries, and improper use of Entity Framework (EF) can lead to significant performance bottlenecks. Without proper pagination, you’ll risk overwhelming both the client and server with large datasets. Inefficient database queries can also severely degrade your API’s response time, while improper use of EF may lead to unnecessary database round-trips and excessive memory consumption.

C#
				[HttpGet("/api/users")]
public async Task<IActionResult> GetUsersAsync()
{
    var users = await _userService.GetAllUsersAsync(); // No pagination, inefficient query
    return Ok(users);
}
			

Here’s how to avoid it: To avoid performance issues, always implement proper pagination and optimize database queries. Use asynchronous methods to keep your API responsive, and leverage EF Core features.

C#
				var pagedResults = await context.Products
    .Where(p => p.Id > lastId)
    .Take(pageSize)
    .ToListAsync();
			
SQL
				SELECT TOP (@pageSize) *
FROM Products
WHERE Id > @lastId
ORDER BY Id;
			

Keyset pagination, An index is used to execute a seek operation at the start of the desired page, It’s not the ideal solution for every situation, but it can be highly beneficial in many scenarios.

Oops, this should actually be three topics

 

7 – Poor documentation

This could ruin your API: Poor documentation leaves consumers in the dark about how to properly use your endpoints, which parameters to pass, or how to handle errors.

C#
				[HttpGet("/api/users/{id}")]
public async Task<IActionResult> GetUserAsync(int id)
{
    //...
}
			

Here’s how to avoid it: Provide clear and thorough documentation that describes the purpose of each endpoint, the required parameters, the expected responses, and how to handle common errors. Tools like Swagger/Scalar/OpenAPI can help automate and standardize this process, making it easier to keep your documentation up to date.

C#
				/// <summary>
/// Retrieves the details of a product by its ID.
/// </summary>
/// <param name="id">The unique identifier of the product.</param>
/// <returns>Returns the product details if found.</returns>
/// <response code="200">Product found</response>
/// <response code="404">Product not found</response>
[HttpGet("/api/users/{id}")]
public async Task<IActionResult> GetUserAsync(int id)
{
    //...
}
			
C#
				builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    // Definindo informações básicas do Swagger
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "My API",
        Version = "v1",
        Description = "An example API to demonstrate Swagger setup",
    });
});

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(options =>
    {
        options.SwaggerEndpoint("/swagger/v1/swagger.json", "My API v1");
    });
}
			

In .NET 9, Swagger is no longer bundled by default. You can manually integrate it or use alternatives like Scalar, which offers a lightweight and modern approach to API documentation. I recommend Scalar for its simplicity and better integration with .NET projects.

 

8 – Ignoring Caching

This could ruin your API: Ignoring caching in your API can significantly degrade performance, especially when dealing with large amounts of data or frequent requests. Without caching, your system is forced to recompute or fetch data repeatedly, which adds unnecessary load and delays.

C#
				[HttpGet("/api/products")]
public async Task<IActionResult> GetAllProductsAsync()
{
    var products = await _productService.GetAllAsync();
    return Ok(products);
}
			

Here’s how to avoid it: Implement caching strategies  like MemoryCache, DistributedCache or HybridCache in .NET 9 to store results for frequently accessed data, reducing the load on your database and improving response times.

C#
				var products = await _cache.GetOrCreateAsync(cacheKey, async entry =>
{
    entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10); // Cache expiration
    
    return await context.Products.ToListAsync();
});
			

 

9 – Ignoring Input Validation.

This could ruin your API: Failing to validate user inputs can lead to security vulnerabilities, incorrect data processing, and overall instability of your API.

C#
				[HttpPost("/api/products")]
public async Task<IActionResult> CreateProductAsync([FromBody] CreateProductRequest product)
{
    //No input validation 
    await _productService.CreateAsync(product);
    return Ok(product);
}
			

Here’s how to avoid it: Always validate inputs to ensure data integrity and prevent malicious inputs. You can use data annotations, custom validation, or frameworks like FluentValidation for more complex scenarios.

C#
				public class ProductValidator : AbstractValidator<Product>
{
    public ProductValidator()
    {
        RuleFor(x => x.Name).NotEmpty().WithMessage("Product name is required.");
        RuleFor(x => x.Price).GreaterThan(0).WithMessage("Price must be greater than zero.");
    }
}

			

By using FluentValidation, we can create more flexible and maintainable input validation, making the API more robust and easier to manage.

C#
				var result = await validator.ValidateAsync(product);

if (!result.IsValid)
{
    // Return validation errors
}
			

 

10 – Failure to Implement Proper Logging

This could ruin your API: Without proper logging, debugging becomes a nightmare, and tracking API usage or monitoring issues in production becomes nearly impossible. Logging is essential for identifying problems and ensuring smooth operations.

Here’s how to avoid it: You can use Serilog, Nlog or other popular logging library, to easily log API activity.

C#
				try
{
    _logger.LogInformation("Fetching user with ID: {UserId}", id);
    
    var user = await _userService.GetByIdAsync(id);
    if (user == null)
    {
        _logger.LogWarning("User with ID {UserId} not found", id);
        return NotFound();
    }
    
    //return
}
catch (Exception ex)
{
    _logger.LogError(ex, "An error occurred while retrieving the user with ID {UserId}", id);
    //return
}
			

 

Conclusion

Avoiding common Web API mistakes can save you from unnecessary headaches and ensure a smoother development process. Now that you’ve learned about these pitfalls and how to address them, it’s time to implement these best practices in your own projects.

If you found this post helpful and want a deeper dive into other API topics like versioning, security, health checks, and more, feel free to let me know! I’d be happy to write a follow-up. Let’s keep improving together and building more efficient APIs!

See you next time!

Share the Post:
plugins premium WordPress