Top 15 Mistakes Developers Make When Creating Web APIs
A warm, beginner-friendly tour of the 15 most common Web API mistakes in ASP.NET Core, with simple fixes, diagrams, tables, and clear C# examples.
A chai stall that takes orders
Picture a busy chai stall outside a railway station. People walk up, shout their orders, pay, and wait for their cup. When the stall is run well, life is smooth. The owner repeats your order back so there is no confusion. He takes money safely. He serves people in a fair line. He keeps the milk fresh and the counter clean. Everyone leaves happy.
Now picture a badly run stall. The owner hears "two chai" but makes one. He keeps cash loose in his pocket and loses track. He serves whoever shouts loudest. He hands out cold, stale tea. Customers get angry and stop coming.
A Web API is just a stall that serves data instead of tea. Other programs walk up, place an order through an HTTP request, and wait for a reply. When your API is run well, clients trust it and keep using it. When it is sloppy, things break in confusing ways, and the bugs are painful to find.
This guide walks through the 15 most common mistakes developers make when building Web APIs. The code uses ASP.NET Core on .NET 10, which is the current LTS release, but the ideas work in any language. Each mistake comes with a simple fix you can use today.
How a client uses your API
Steps
Client asks
Sends an HTTP request
API checks
Auth and validation
API runs
Business logic and data
API answers
JSON plus a status code
Mistake 1: Lying with status codes
This is the most common mistake of all. Many APIs return 200 OK for everything, even failures, then hide the real result inside the body. The caller has to read the body and guess whether it worked.
The HTTP status code is your stall owner repeating the order back. It should tell the truth at a glance. Browsers, caches, and monitoring tools all read it.
| What happened | Right status code |
|---|---|
| Read worked | 200 OK |
| Created a new thing | 201 Created |
| Bad input from the client | 400 Bad Request |
| Not logged in | 401 Unauthorized |
| Logged in but not allowed | 403 Forbidden |
| Thing not found | 404 Not Found |
| Your server crashed | 500 Internal Server Error |
In ASP.NET Core, the helper methods make this easy.
[HttpGet("{id:int}")]
public async Task<IActionResult> GetBook(int id)
{
var book = await _service.FindAsync(id);
if (book is null)
return NotFound(); // sends 404
return Ok(book); // sends 200
}Mistake 2: Returning database entities directly
It feels quick to return your Entity Framework entity straight from the controller. But this ties your public API to your private database tables. Rename a column and you break clients. Add a PasswordHash field and it can leak out.
Use a DTO (Data Transfer Object), a small class that holds only the fields you want to share.
public record BookDto(int Id, string Title, string Author);
public BookDto ToDto(Book b) =>
new(b.Id, b.Title, b.Author); // no PasswordHash, no secretsMistake 3: Blocking the thread with synchronous calls
Each web server has a limited pool of worker threads. When you call the database synchronously, the thread sits and waits, doing nothing, until the answer comes back. Under load, every thread gets stuck waiting, and new requests have nowhere to run. This is called thread pool starvation.
The fix is async and await. While the thread waits for the database, it is freed to serve someone else.
// Bad: blocks the thread
var book = _db.Books.First(b => b.Id == id);
// Good: frees the thread while waiting
var book = await _db.Books.FirstOrDefaultAsync(b => b.Id == id);Mistake 4: Skipping input validation
Never trust what comes in. A missing title, a negative price, or a 5000-character name can break your code or open a security hole. Validate every request before you use it.
ASP.NET Core supports data annotations out of the box.
public record CreateBook
{
[Required, StringLength(120)]
public string Title { get; init; } = "";
[Range(1, 9999)]
public decimal Price { get; init; }
}When validation fails, ASP.NET Core automatically returns a 400 Bad Request with a clear list of problems. For richer rules, libraries like FluentValidation are popular and free.
Mistake 5: Leaking stack traces in error messages
When your code crashes, never send the full stack trace to the client. It exposes file paths, library versions, and clues an attacker can use. It also confuses normal users.
Use a global error handler so one crash does not spill secrets. ASP.NET Core has IExceptionHandler and the Problem Details format (RFC 9457) for clean, standard error bodies.
app.UseExceptionHandler();
app.UseStatusCodePages();
builder.Services.AddProblemDetails();In development you can show details. In production you show a short, safe message and log the real error on the server.
Mistake 6: No pagination on large lists
GET /api/books looks innocent until your table has a million rows. Returning everything at once is slow, eats memory, and can crash both your server and the client.
Always paginate large collections. Accept a page number and a page size, and cap the size so nobody can ask for ten million rows.
[HttpGet]
public async Task<IActionResult> GetBooks(int page = 1, int size = 20)
{
size = Math.Clamp(size, 1, 100); // never let size run wild
var items = await _db.Books
.OrderBy(b => b.Id)
.Skip((page - 1) * size)
.Take(size)
.ToListAsync();
return Ok(items);
}Mistake 7: Putting all logic inside controllers
A controller should be thin. Its job is to read the request, call something, and return a response. When you stuff validation, business rules, and database code all into the controller, it becomes a giant mess that is hard to test and hard to change.
Move the real work into services or handlers. Keep the controller as a friendly receptionist, not the whole office.
Keep the controller thin
Steps
Controller
Reads request, returns reply
Service
Business rules
Repository
Talks to data
Database
Stores the data
A quick note on tooling. Some popular libraries for this style, like MediatR and MassTransit, moved to a commercial license in their newer versions. They are still fine to use, but check the license and pricing before you adopt them in a paid product. You do not need them to keep controllers thin; a plain service class works perfectly.
Mistake 8: Hardcoding secrets and config values
Typing a database password or an API key straight into your code is risky. It ends up in source control for everyone to see, and you cannot change it without a redeploy.
Use configuration instead. Read values from appsettings.json, environment variables, or a secret store like Azure Key Vault. Keep real secrets out of the repository.
var connection = builder.Configuration
.GetConnectionString("Default"); // from config, not hardcodedMistake 9: Ignoring authentication and authorization
Authentication asks "who are you?" Authorization asks "are you allowed to do this?" Many new APIs skip both and leave every endpoint wide open. That is like leaving the cash box on the counter.
Protect endpoints with the [Authorize] attribute, and use HTTPS everywhere, even for internal APIs. Let's Encrypt certificates are free.
| Term | Question it answers | Example |
|---|---|---|
| Authentication | Who are you? | Logging in with a token |
| Authorization | What can you do? | Only admins can delete |
| HTTPS | Is the line private? | Encrypts the traffic |
[Authorize(Roles = "Admin")]
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id)
{
await _service.DeleteAsync(id);
return NoContent(); // 204
}Mistake 10: No rate limiting
Without limits, one buggy client or one attacker can flood your API with thousands of requests and knock it over for everyone. ASP.NET Core has built-in rate limiting since .NET 7.
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
});
});
app.UseRateLimiter();This says "each window allows 100 requests per minute." Extra requests get a polite 429 Too Many Requests instead of crashing your stall.
Mistake 11: The N+1 query problem
This one is sneaky. You load a list of authors, then loop over them and load each author's books one by one. One query becomes one hundred queries. The page feels slow and the database groans.
Fix it by loading related data in one go with Include, or by selecting just what you need.
var authors = await _db.Authors
.Include(a => a.Books) // one query, not N+1
.ToListAsync();Mistake 12: Inconsistent naming and verbs in URLs
A URL should point to a thing (a resource), not an action. The HTTP verb already says what you are doing. Mixing styles confuses everyone.
| Don't | Do | Why |
|---|---|---|
GET /getAllBooks | GET /api/books | Verb is already in the method |
POST /createBook | POST /api/books | Cleaner and predictable |
GET /api/Book/{id} | GET /api/books/{id} | Use plural, lowercase nouns |
Pick one style for the whole API and stick to it. When the pattern is steady, clients can guess the next endpoint correctly.
Mistake 13: No API versioning
The day will come when you must rename a field or change a rule. Without a version, that change breaks every existing caller at once. With a version, old clients keep using /v1 while new ones move to /v2.
Add the free Asp.Versioning.Mvc package early.
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
});Now you can ship breaking changes safely, on your schedule, without a panic.
Mistake 14: Forgetting logging and monitoring
When something breaks in production at 2 a.m., logs are your only flashlight. Many APIs log nothing useful, so a bug becomes an all-night guessing game.
Use structured logging so you can search logs by fields like user id or request id. Add health checks so a load balancer knows when your API is sick. The modern standard for tracing across services is OpenTelemetry, which ASP.NET Core supports well.
_logger.LogInformation(
"Book {BookId} fetched for user {UserId}", id, userId);Structured fields like {BookId} let you filter logs later instead of grepping plain text.
Mistake 15: Not testing the API
Shipping without tests means every change is a gamble. A small fix can quietly break three other endpoints, and you only find out when a user complains.
Write tests at two levels. Unit tests check one service or rule in isolation. Integration tests spin up the API in memory with WebApplicationFactory and check real request and response behavior.
Two layers of API tests
Steps
Unit tests
One rule at a time
Integration tests
Full request to response
Confidence
Ship without fear
[Fact]
public async Task Get_Returns_200_For_Existing_Book()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/books/1");
response.EnsureSuccessStatusCode(); // expects 2xx
}How the mistakes connect
Many of these mistakes feed each other. Skipping validation leads to crashes, which leak stack traces, which then need a global error handler. No pagination plus an N+1 query makes a slow API even slower. Fixing them together builds an API that is safe, fast, and easy to trust.
Quick recap
- Let the status code tell the truth. Do not return 200 for failures.
- Return DTOs, not raw database entities, to protect and stabilize your API.
- Use async and await so threads are not blocked waiting on the database.
- Validate every incoming request before you trust it.
- Never leak stack traces; use a global handler and Problem Details.
- Paginate large lists and cap the page size.
- Keep controllers thin; move logic into services.
- Read secrets from configuration, never hardcode them.
- Add authentication, authorization, and HTTPS from the start.
- Add rate limiting so one client cannot flood you.
- Avoid the N+1 query trap with
Includeor projection. - Use clear, plural, noun-based URLs and consistent naming.
- Add API versioning early so changes do not break clients.
- Set up structured logging, health checks, and OpenTelemetry.
- Write unit and integration tests so changes are safe.
References and further reading
- ASP.NET Core Best Practices — Microsoft Learn
- Web API Design Best Practices — Azure Architecture Center
- Handle errors in ASP.NET Core APIs — Microsoft Learn
- Rate limiting in ASP.NET Core — Microsoft Learn
- ASP.NET Core Web API Best Practices — Code Maze
Related Posts
The 5 Most Common REST API Design Mistakes (and How to Avoid Them)
A warm beginner guide to the 5 most common REST API design mistakes in ASP.NET Core, with simple fixes, diagrams, tables, and clear C# code examples.
Best Practices for Building REST APIs in ASP.NET Core
A friendly, beginner guide to REST API best practices in ASP.NET Core with naming, status codes, validation, ProblemDetails, paging, versioning, security, and code.
Problem Details for ASP.NET Core APIs: A Beginner's Guide
Learn Problem Details in ASP.NET Core step by step. Give your API one clean error format with RFC 9457, AddProblemDetails, and IExceptionHandler in .NET 10.
How to Increase the Performance of Web APIs in .NET
A friendly, step-by-step guide to making your ASP.NET Core Web APIs fast: async, caching, query tuning, compression, and pooling in .NET 10.
CORS in ASP.NET Core: A Comprehensive Guide
A simple, friendly guide to CORS in ASP.NET Core. Learn how the browser, preflight requests, and policies work, with clear diagrams, tables, and code.
Authentication and Authorization Best Practices in ASP.NET Core
A friendly guide to authentication and authorization in ASP.NET Core for .NET 10 — JWT, cookies, claims, roles, policies, and security best practices with diagrams.