DEV Community

Cover image for TOP 15 Mistakes Developers Make When Creating Web APIs
Anton Martyniuk
Anton Martyniuk

Posted on • Originally published at antondevtips.com on

TOP 15 Mistakes Developers Make When Creating Web APIs

In this blog post, I'll walk you through the top 15 mistakes developers often make when building Web APIs in .NET.
I will show you solutions to avoid and fix these mistakes.

Let's dive in!

On my website: antondevtips.com I share .NET and Architecture best practices.
Subscribe to become a better developer.

1. Not Using API Versioning

Mistake:
Without versioning, changes to your API can break existing client applications that rely on older endpoints.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/api/products", async (IProductService service) =>
{
    var products = await service.GetProductsAsync();
    return Results.Ok(products);
});

app.Run();
Enter fullscreen mode Exit fullscreen mode

Solution:
Implement API versioning using URL segments, query parameters, or headers to ensure backward compatibility and smooth transitions between versions.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1,0);
    options.AssumeDefaultVersionWhenUnspecified = true;
});

var app = builder.Build();

app.MapGet("/api/v1/products", async (IProductService service) =>
{
    var products = await service.GetProductsAsync();
    return Results.Ok(products);
});

app.Run();
Enter fullscreen mode Exit fullscreen mode

2. Poor Error Handling

Mistake:
Poor error messages make it tough for API consumers to diagnose what went wrong.

app.MapGet("/api/v1/order/{id}", (int id) =>
{
    if (id < 0)
    {
        return Results.Problem();
    }

    return Results.Ok(order);
});
Enter fullscreen mode Exit fullscreen mode

Solution:
Use standardized error responses like ProblemDetails and appropriate HTTP status codes to help clients understand and resolve problems.

app.MapGet("/api/v1/order/{id}", (int id) =>
{
    if (id < 0)
    {
        return Results.Problem(
            detail: "The order ID provided is invalid.",
            statusCode: StatusCodes.Status400BadRequest,
            title: "Invalid Request"
        );
    }

    return Results.Ok(order);
});
Enter fullscreen mode Exit fullscreen mode

3. Lack of Authentication and Authorization

Mistake:
Failing to secure your API exposes it to unauthorized access and potential data breaches.

// Program.cs - Mistake (no authentication setup, open endpoints)
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Anyone can call this endpoint without any credentials
app.MapGet("/api/orders", async (IOrder service) =>
{
    var orders = await service.GetOrdersAsync();
    return Results.Ok(orders);
});

app.Run();
Enter fullscreen mode Exit fullscreen mode

Solution:
Implement robust authentication and authorization mechanisms, such as OAuth 2.0 or JWT, to protect your API endpoints.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes("your-secret-key"))
        };
    });

var app = builder.Build();

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

app.MapGet("/api/orders", async (IOrder service) =>
{
    var orders = await service.GetOrdersAsync();
    return Results.Ok(orders);
}).RequireAuthorization();

app.Run();
Enter fullscreen mode Exit fullscreen mode

4. Ignoring Asynchronous Programming

Mistake:
Synchronous code can lead to thread blocking and preventing them from handling other requests.
This reduces performance significantly.

app.MapGet("/api/v1/products", (IProductService productService) =>
{
    var products = productService.GetProducts();
    return Ok(products);
});
Enter fullscreen mode Exit fullscreen mode

Solution:
Use async and await keywords paired with asynchronous methods, improving scalability of web server.

Asynchronous programming prevents your application from blocking threads while waiting for I/O operations to complete.
Among these operations: reading files, waiting for database results, waiting for API call results, etc.

app.MapGet("/api/v1/products", async (IProductService productService) =>
{
    var products = await productService.GetProductsAsync();
    return Ok(products);
});
Enter fullscreen mode Exit fullscreen mode

5. Not Following RESTful Conventions

Mistake:
Ignoring RESTful principles can lead to inconsistent and non-standard APIs.

// Not following REST: using GET for deletion
app.MapGet("/api/deleteUser?id=123", () =>
{
    // Delete logic here
    return Results.Ok("Deleted");
});
Enter fullscreen mode Exit fullscreen mode

Solution:
Design your API following RESTful conventions, using appropriate HTTP methods (GET, POST, PUT, DELETE, etc.) and status codes.

app.MapGet("/api/users/{id}", (int id) =>
{
    var user = ...;
    return Results.Ok(user);
});

app.MapPost("/api/users", (CreateUserRequest request) =>
{
    var user = request.MapToEntity();
    return Results.Created(user);
});

app.MapPut("/api/users/{id}", (int id, UpdateUserRequest request) =>
{
    // Update User
    return Results.NoContent();
});

app.MapDelete("/api/users/{id}", (int id) =>
{
    // Delete User    
    return Results.NoContent();
});
Enter fullscreen mode Exit fullscreen mode

6. Not Validating Input Data

Mistake:
Accepting unvalidated input can lead to security vulnerabilities and data integrity issues.

app.MapPost("/api/users", (CreateUserRequest request) =>
{
    // No validation performed
    return Results.Ok(user);
});
Enter fullscreen mode Exit fullscreen mode

Solution:
Validate all incoming data using Data Annotations or Fluent Validation.
Always be pessimistic about data your APIs consume.

app.MapPost("/api/users", (CreateUserRequest request,
    IValidator<CreateUserRequest> validator) =>
{
    var validationResult = await validator.ValidateAsync(request);
    if (!validationResult.IsValid)
    {
        return Results.ValidationProblem(validationResult.ToDictionary());
    }

    return Results.Ok(user);
});
Enter fullscreen mode Exit fullscreen mode

7. Ignoring Security Best Practices

Mistake:
Ignoring security measures can expose your API to attacks like SQL injection or cross-site scripting.

app.MapGet("/api/product/{id}", (string name) =>
{
    // Vulnerable SQL string concatenation
    var command = new SqlCommand(
        "SELECT * FROM Products WHERE Name = " + name, connection);

    connection.Open();
    using var reader = command.ExecuteReader();
    // ...
    return Results.Ok();
});
Enter fullscreen mode Exit fullscreen mode

This API endpoint is vulnerable to SQL injection, as name parameter can accept any string like an SQL command.

Solution:
Follow security best practices, such as using parameterized queries, encrypting sensitive data, and enforcing HTTPS.

// Use safe methods from ORM like EF Core or Dapper
// Or use parameters to prevent SQL injection
app.MapGet("/api/product/{id}", (string name, ProductDbContext dbContext) =>
{
    var products = await dbContext.Products
        .Where(x => x.Name === name)
        .ToListAsync();

    return Results.Ok(products);
});
Enter fullscreen mode Exit fullscreen mode

8. Poor Logging and Monitoring

Mistake:
Without proper logging, diagnosing issues becomes challenging, especially in distributed systems.

Solution:
Add Open Telemetry and Logging to your application to track application metrics, traces and logs.
Use tools like Serilog for logging and Seq or Jaeger to view distributed traces.
You can use Seq both for logs and distributed traces.

Consider these free tools for monitoring:

  • Jaeger (distributed traces)
  • Loki (logs)
  • Prometheus and Grafana (metrics)
  • Seq (logs and distributed traces) (free only for a single user)
  • .NET Aspire (logging, metrics, distributed traces)

You can also use cloud-based solutions for monitoring your applications like Application Insights.

builder.Services
    .AddOpenTelemetry()
    .ConfigureResource(resource => resource.AddService("ShippingService"))
    .WithTracing(tracing =>
    {
        tracing
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddEntityFrameworkCoreInstrumentation()
            .AddRedisInstrumentation()
            .AddNpgsql();

        tracing.AddOtlpExporter();
    });
Enter fullscreen mode Exit fullscreen mode

9. Lack of API Documentation

Mistake:
Without clear documentation, clients struggle to understand how to use your API.

Solution:
Provide thorough documentation using tools like Swagger/OpenAPI to generate interactive API docs.

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.MapGet("/api/products", () => new[] { "Product1", "Product2" });

app.Run();
Enter fullscreen mode Exit fullscreen mode

10. Not Optimizing Database and Data Access Layer

Mistake:
Unoptimized database queries and inefficient data access code can lead to slow performance and scalability issues.

// Fetching ALL columns
var book = await context.Books
    .Include(b => b.Author)
    .FirstOrDefaultAsync(b => b.Id == id, cancellationToken);
Enter fullscreen mode Exit fullscreen mode

Solution:
Optimize your database by indexing, optimizing queries, and using efficient data access patterns in EF Core or Dapper.

// Fetching only needed columns without tracking
var book = await context.Books
    .Include(b => b.Author)
    .Where(b => b.Id == id)
    .Select(b => new BooksPreviewResponse
    {
        Title = b.Title, Author = b.Author.Name, Year = b.Year
    })
    .FirstOrDefaultAsync(cancellationToken);
Enter fullscreen mode Exit fullscreen mode

11. Returning Too Much Data Without Paging, Filtering, or Sorting

Mistake:
Sending large datasets can lead to performance bottlenecks and increased bandwidth usage.

// Selecting all books (entire database)
var allBooks = await context.Books
    .Include(b => b.Author)
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

Solution:
Implement paging, filtering, and sorting mechanisms to allow clients to retrieve only the data they need.

// Use paging to select fixed number of records
int pageSize = 50;
int pageNumber = 1;

var books = context.Books
    .AsNoTracking()
    .OrderBy(p => p.Title)
    .Skip((pageNumber - 1) * pageSize)
    .Take(pageSize)
    .ToList();
Enter fullscreen mode Exit fullscreen mode

12. Not Using Caching

Mistake:
Not using cache for frequently accessed data can result in unnecessary database hits and slower response times.

Solution:
Implement caching strategies using in-memory caches like MemoryCache/HybridCache (.NET 9) or distributed caches like Redis to improve performance.

builder.Services.AddHybridCache();

[HttpGet("orders/{id}")]
public async Task<IActionResult> GetOrderAsync(int id,
    [FromServices] IHybridCache cache)
{
    string cacheKey = $"Order_{id}";
    var order = await cache.GetOrCreateAsync(cacheKey, async entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
        using var context = new AppDbContext();
        return await context.Orders.FindAsync(id);
    });

    if (order is null)
    {
        return NotFound();
    }

    return Ok(order);
}
Enter fullscreen mode Exit fullscreen mode

13. Returning Big Payloads Without Compression

Mistake:
Large uncompressed responses consume more bandwidth and can slow down data transfer rates.

Solution:
Compressing your responses with Brotli or GZIP can significantly reduce payload size.
Smaller responses mean faster data transfer and a better user experience.

Brotli and Gzip reduce the size of the outgoing JSON, HTML or Static files data.
Adding them early in the pipeline will ensure smaller payloads.

builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();
});

builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
    options.Level = System.IO.Compression.CompressionLevel.Fastest;
});

builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
    options.Level = System.IO.Compression.CompressionLevel.Fastest;
});

var app = builder.Build();
app.UseResponseCompression();
Enter fullscreen mode Exit fullscreen mode

14. Fat Controllers

Mistake:
Overloaded controllers with too much logic and methods become hard to maintain and test.

public class ProductsController(
    IProductRepository productRepository,
    ILoggingService loggingService,
    ICacheService cacheService,
    IEmailService emailService,
    IAuthenticationService authService,
    IReportGenerator reportGenerator,
    IFeatureFlagService featureFlagService
) : ControllerBase
{
    public IActionResult GetAllProducts() { }
    public IActionResult GetProductById(int id) { }
    public IActionResult CreateProduct() { }
    public IActionResult UpdateProduct(int id) { }
    public IActionResult DeleteProduct(int id) { }
    public IActionResult GetProductsByCategory(string category) { }
    public IActionResult ExportProducts()  { }
    public IActionResult SendProductNewsletter()  { }
    public IActionResult GetProductStats()  { }
    public IActionResult GetProductRecommendations() { }
}
Enter fullscreen mode Exit fullscreen mode

Solution:
Apply the Single Responsibility Principle by moving business logic into services or other layers.
Break big controllers to smaller ones - keeping them focused on one thing or entity.

// For example, break them into specialized controllers or Minimal APIs
// ProductController - focuses on product entity
public record Product(int Id, string Name);

[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAllProducts() 
    {
        // ...
    }

    [HttpGet("{id}")]
    public IActionResult GetProductById(int id) 
    {
        // ...
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

15. Not Using Minimal APIs

Mistake:
Controllers can quickly become out of order with too much logic inside.

Solution:
Replace Controllers with Minimal APIs or Fast Endpoints creating a new class per endpoint.
This makes endpoints single responsible and not coupled with other endpoints.

Minimal APIs in .NET 9 received a huge performance boost and can process 15% more requests per second than in .NET 8.
Also, Minimal APIs consume 93% less memory compared to a previous version.

app.MapGet("/api/v1/products", async (IProductService service) =>
{
    var products = await service.GetProducts();
    return Results.Ok(products);
});

app.MapGet("/api/orders", async (IOrder service) =>
{
    var orders = await service.GetOrders();
    return Results.Ok(orders);
});
Enter fullscreen mode Exit fullscreen mode

Summary

By avoiding these 15 common pitfalls - your .NET Web APIs will be more reliable, secure, and scalable.

  • Keep your endpoints versioned for backward compatibility.
  • Offer meaningful error messages.
  • Secure your APIs with authentication and authorization.
  • Use async/await for better scalability and performance.
  • Stick to RESTful conventions.
  • Validate input, be pessimistic about data you receive.
  • Follow security best practices.
  • Log and monitor your applications.
  • Document APIs with Swagger/OpenAPI.
  • Optimize your data access.
  • Use paging/filtering/sorting for large datasets.
  • Cache often-used data.
  • Compress large responses.
  • Keep controllers small and lean.
  • Use Minimal APIs for speed and simplicity

On my website: antondevtips.com I share .NET and Architecture best practices.
Subscribe to become a better developer.

Top comments (0)