Skip to main content
SEMastery
ASP.NETbeginner

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.

12 min readUpdated May 31, 2026

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

Client asks
API checks
API runs
API answers

Steps

1

Client asks

Sends an HTTP request

2

API checks

Auth and validation

3

API runs

Business logic and data

4

API answers

JSON plus a status code

A request goes in, your code runs, and an honest answer comes back.

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 happenedRight status code
Read worked200 OK
Created a new thing201 Created
Bad input from the client400 Bad Request
Not logged in401 Unauthorized
Logged in but not allowed403 Forbidden
Thing not found404 Not Found
Your server crashed500 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 secrets
A DTO sits between your database and the outside world.

Mistake 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);
Async frees the thread while the database works.

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

Controller
Service
Repository
Database

Steps

1

Controller

Reads request, returns reply

2

Service

Business rules

3

Repository

Talks to data

4

Database

Stores the data

The controller passes work to a service, which does the heavy lifting.

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 hardcoded

Mistake 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.

TermQuestion it answersExample
AuthenticationWho are you?Logging in with a token
AuthorizationWhat can you do?Only admins can delete
HTTPSIs 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.

N+1 fires one query per item instead of one query total.

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'tDoWhy
GET /getAllBooksGET /api/booksVerb is already in the method
POST /createBookPOST /api/booksCleaner 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

Unit tests
Integration tests
Confidence

Steps

1

Unit tests

One rule at a time

2

Integration tests

Full request to response

3

Confidence

Ship without fear

Fast unit tests for logic, integration tests for real requests.
[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.

Good habits stack up into a healthy API.

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 Include or 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

Related Posts