Building APIs That Don’t Rot: Lessons from Shipping Production Systems in C
Rotten APIs are everywhere, and I’ve been guilty of shipping more than a few. The problems rarely show up on day one. Instead, they creep in as features grow, teams change, and edge cases pile up. Here’s what I’ve learned (the hard way) about designing APIs in C#/.NET that survive more than one release cycle.
API Design: Pragmatism Over Purity
REST is great on paper, but most real APIs end up somewhere between “textbook REST” and “just make it work.” I used to obsess over purity, but I’ve learned to prioritize:
Consistent resource naming, even if you bend REST rules occasionally
Returning actionable error messages, not just HTTP status codes
Making endpoints idempotent by default, especially for write operations
For example, when building a payment API, we made the POST /payments endpoint idempotent by requiring a client-generated Idempotency-Key. This stopped duplicate charges when clients retried requests, but it also forced us to store and manage those keys. The trade-off? Slightly more backend complexity, but far fewer support tickets about “why did I get double-charged?”
Structuring .NET Projects for Sanity (and Scale)
“Just put it in the Controllers folder” is how most projects start. But once your codebase hits double-digit projects, this falls apart fast. Clean Architecture saved us when the business logic got gnarly. What actually worked:
Separate core domain logic from infrastructure and API layers
Use interfaces to decouple data access (
IOrderRepository, notOrderDbContext)Lean on dependency injection for testability and swapping out implementations
Here’s a simplified example of a command handler for creating an order:
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, OrderResult>
{
private readonly IOrderRepository _repository;
public CreateOrderHandler(IOrderRepository repository)
{
_repository = repository;
}
public async Task<OrderResult> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
var order = new Order(request.CustomerId, request.Items);
await _repository.AddAsync(order, cancellationToken);
return new OrderResult(order.Id, "Order created");
}
}
This pattern kept our core logic testable and portable, even when we swapped out SQL for CosmosDB later.
Deployment Isn’t Done When It Works on Your Machine
Early on, I treated deployment like a black box. AWS or Azure would “just work,” right? Wrong. The most painful bugs I’ve debugged were caused by:
Missing environment variables in production
Accidentally deploying dev branches to staging (or worse, prod)
Not setting up proper logging or alerting, leaving us blind when things failed
The fix? Automate everything you can, and make environment separation a first-class concern. We now use explicit config files per environment and lock down deployment pipelines so only tested branches get near production. This isn’t glamorous, but it’s saved us from the “it worked yesterday, but prod is on fire” scenario more than once.
AI Integrations: Useful, Not Magical
When we first integrated OpenAI into our product, we treated prompt design like software config, not magic. Every prompt (and its expected output shape) lives in version-controlled files, and we run integration tests just like with any other external dependency. The biggest lesson: don’t let AI turn your codebase into spaghetti. Always wrap AI calls with interfaces and keep business logic out of your prompt strings.
Example interface:
public interface ITextSummarizer
{
Task<string> SummarizeAsync(string text, CancellationToken cancellationToken);
}
This abstraction let us swap between OpenAI, Azure Cognitive Services, or even mock implementations for testing.
Actionable Takeaway
Next time you’re designing an API, force yourself to write the error responses and idempotency logic before you ship. You’ll thank yourself the first time your frontend retries a request and doesn’t break something in production.
What’s the one design decision in your last project you wish you could go back and redo? Share your scars, not just your successes. Let’s learn from each other.
Top comments (0)