🚀 Coming Soon: Enforcing Code Style A brand-new course is launching soon inside The CodeMan Community!
Join now to lock in early access when it drops - plus get everything else already inside the group.
Founding Member Offer:
Let’s be honest:
Building APIs sounds easy… until you have to maintain them.
I've seen beautifully-coded APIs crash and burn simply because of tiny decisions made early on - things you don't even notice until your API is under real-world pressure.
Let’s break down the 5 most common mistakes developers make when building APIs - and how you can avoid falling into these traps, with real .NET 9 examples along the way.
The Mistake:
Trusting that clients will always send valid data.
Reality:
If you don’t validate input properly, your API becomes vulnerable to bugs, crashes, and even security risks.
Bad Example (Before - Trusting Input):
app.MapPost("/users", async (UserDto user) =>{ // Assuming user.Name and user.Email are always provided... var newUser = new User { Name = user.Name, Email = user.Email }; await dbContext.Users.AddAsync(newUser); await dbContext.SaveChangesAsync(); return Results.Ok();});
Note: Don't put logic and database call directly in controllers/endpoints.
Problem:
Better Example (After - Defensive API Design):
app.MapPost("/users", async (UserDto user) =>{ if (string.IsNullOrWhiteSpace(user.Name) || string.IsNullOrWhiteSpace(user.Email)) return Results.BadRequest("Name and Email are required."); if (!user.Email.Contains("@")) return Results.BadRequest("Invalid email format."); var newUser = new User { Name = user.Name.Trim(), Email = user.Email.Trim() }; await dbContext.Users.AddAsync(newUser); await dbContext.SaveChangesAsync(); return Results.Ok(newUser);});
Or even better? Use FluentValidation or a custom ValidatorService to avoid cluttering your endpoints!
Why It’s Important:
The Mistake:
Releasing APIs with no version control because "we’ll fix it later."
Reality:
You will need to change APIs. And when you do, you’ll regret not planning for versions.
Bad Example (Before - No Versioning):
app.MapGet("/products", () =>{ // returns products});
Problem:
When your response structure changes, older clients break immediately.
Better Example (After - Versioned API):
app.MapGroup("/api/v1") .MapGet("/products", () => { // returns products v1 }); app.MapGroup("/api/v2") .MapGet("/products", () => { // returns products v2 with improvements });
Or using .RequireHost() for different subdomains if you want real-world production scaling.
Why It’s Important:
The Mistake:
Returning 200 OK for everything — even when things fail.
Reality:
HTTP status codes exist for a reason: communication.
Bad Example (Before - Everything is OK):
return Results.Ok("User not found.");
Problem:
Client sees 200. But the user doesn’t exist. Confusing! Now they have to parse the message string. Bad practice.
Better Example (After - Correct Status Codes):
var user = await dbContext.Users.FindAsync(id);if (user is null) return Results.NotFound($"User with id {id} not found."); return Results.Ok(user);
Use:
Why It’s Important:
The Mistake:
Returning entire database entities or gigantic nested models.
Reality:
Clients usually need a small slice of data, not your entire database schema.
Bad Example (Before - Entity Dumping):
app.MapGet("/orders", async (DbContext db) =>{ var orders = await db.Orders.ToListAsync(); return Results.Ok(orders);});
Problem:
Better Example (After - DTO Mapping):
app.MapGet("/orders", async (DbContext db) =>{ var orders = await db.Orders .Select(order => new { order.Id, order.CustomerName, order.TotalAmount, order.OrderDate }) .ToListAsync(); return Results.Ok(orders);});
Why It’s Important:
The Mistake:
Scattering try-catch blocks randomly, or worse, letting unhandled exceptions bubble up.
Reality:
You need one single place to handle unexpected errors cleanly.
Bad Example (Before - Scattered Try-Catch):
try{ var user = await dbContext.Users.FindAsync(id); return Results.Ok(user);}catch (Exception ex){ return Results.Problem(ex.Message);}
Problem:
Better Example (After - Global Exception Handling Middleware):
app.UseExceptionHandler(errorApp =>{ errorApp.Run(async context => { context.Response.StatusCode = 500; context.Response.ContentType = "application/json"; await context.Response.WriteAsJsonAsync(new { Error = "Something went wrong. Please try again later." }); });});
Or even better? Use ProblemDetails (application/problem+json) in .NET 9 automatically via Problem() responses.
Why It’s Important:
But here's the thing:
I’m not advising you to use exceptions as your primary way of handling errors.
Instead, if you’re building new APIs today, strongly consider the Result pattern (like Result
That way you explicitly return success/failure results without relying on exceptions at all.
Exceptions should be for exceptional, truly unexpected cases - not regular validation errors.
That said, if your project (or your team) already uses exceptions, the next best thing is centralizing how you handle them.
If you’re curious, here’s a super simple Result-style error handling:
public record Result<T>(bool IsSuccess, T? Value, string? ErrorMessage); app.MapGet("/users/{id}", async (Guid id, DbContext db) =>{ var user = await db.Users.FindAsync(id); if (user is null) return Results.NotFound(new Result<User>(false, null, "User not found")); return Results.Ok(new Result<User>(true, user, null));});
If you're serious about building clean, real-world RESTful APIs in .NET - the way we discussed throughout this post - I highly recommend checking out the new Pragmatic RESTful APIs in .NET course by Milan Jovanovic.
This isn't a sponsored recommendation - it's genuinely the best material I've seen on the topic.
You’ll even get a discount through my affiliate link. Highly worth it if you want to take your API skills to the next level!
For building better APIs, check out API Versioning, Rate Limiting, and API Key Authentication.
APIs aren't just about "sending data back and forth." APIs are contracts. APIs are promises.
When you build APIs defensively - with good validation, versioning, error handling, and thoughtful responses - you're making a promise to your clients (and your future self) that your system will be reliable and predictable.
Even small improvements in your API hygiene now can save you dozens (or hundreds) of hours later.
That's all from me today.
P.S. Follow me on YouTube.
Stop arguing about code style. In this course you get a production-proven setup with analyzers, CI quality gates, and architecture tests — the exact system I use in real projects. Join here.
Not sure yet? Grab the free Starter Kit — a drop-in setup with the essentials from Module 01.
Design Patterns that Deliver — Solve real problems with 5 battle-tested patterns (Builder, Decorator, Strategy, Adapter, Mediator) using practical, real-world examples. Trusted by 650+ developers.
Just getting started? Design Patterns Simplified covers 10 essential patterns in a beginner-friendly, 30-page guide for just $9.95.
Every Monday morning, I share 1 actionable tip on C#, .NET & Architecture that you can use right away. Join here.
Join 20,000+ subscribers who mass-improve their .NET skills with actionable tips on C#, Software Architecture & Best Practices.
Subscribe to the TheCodeMan.net and be among the 20,000+ subscribers gaining practical tips and resources to enhance your .NET expertise.