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();
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();
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);
});
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);
});
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();
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();
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);
});
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);
});
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");
});
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();
});
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);
});
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);
});
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();
});
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);
});
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();
});
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();
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);
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);
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();
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();
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);
}
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();
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() { }
}
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)
{
// ...
}
// ...
}
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);
});
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)