REST API ASP.NET Core — Silent 500s from Middleware Order
All endpoints returned 500 with empty body in production because UseExceptionHandler after UseRouting.
20+ years shipping production .NET services in enterprise systems. Written from production experience, not tutorials.
- ASP.NET Core is a cross-platform framework for building high-performance REST APIs
- Key components: Controllers, Routing, Middleware, Dependency Injection, Configuration
- Kestrel web server handles millions of requests with minimal overhead
- Missing middleware ordering (e.g., exception handler after routing) causes silent 500 errors
- Biggest mistake: assuming [ApiController] handles all validation automatically—model state errors can still slip through
- Performance insight: async I/O and response compression reduce latency by 30-50% under load
Imagine you own a restaurant. You don't let customers walk into the kitchen — they order through a waiter who brings back exactly what they asked for. A REST API is that waiter: your frontend (the customer) sends a request, the API fetches or saves data, and returns a structured response. ASP.NET Core is the kitchen — fast, organized, and capable of serving thousands of orders at once. Every app you've ever used — Uber, Spotify, your bank — has a 'waiter' like this running behind the scenes.
Build a REST API in ASP.NET Core and you've built the backbone of most modern apps. It's how your React frontend talks to your database, how mobile apps fetch user data, how third-party services integrate with your platform. Microsoft rebuilt ASP.NET from scratch in 2016 to be cross-platform, fast, and genuinely pleasant to work with. Startups and enterprises alike rely on it for millions of daily requests.
The problem it solves: you need a reliable HTTP layer that handles requests, validates input, talks to a database, and returns meaningful responses — without turning into a maintenance nightmare six months in. ASP.NET Core gives you a clean middleware pipeline, built-in DI, and attribute routing. But the magic comes with traps. Middleware order matters. [ApiController] doesn't validate everything. Route constraints can return 404s when you expect 400s.
By the end of this article, you'll understand every piece — routing, controllers, DTOs, error handling, DI — and the real-world patterns that separate senior engineers from copy-paste coders. You'll also learn why each piece exists, so you can make architectural decisions based on trade-offs, not boilerplate.
Here's the thing most guides skip: the subtle mistakes that cause silent outages. That 404 that should be a 200? Wrong middleware order. That 500 with no log? Exception handler after routing. That validation passing invalid data? [ApiController] doesn't validate parameters from [FromQuery]. Senior engineers don't just know the syntax — they know where the framework lies to you.
What is REST API with ASP.NET Core?
ASP.NET Core REST APIs expose your backend logic over HTTP. Each request flows through a middleware pipeline — a sequence of components handling cross-cutting concerns like logging, CORS, authentication, and routing. The pipeline order is everything: get it wrong and your exception handler may never run. Kestrel, the web server, is fast and lightweight, capable of handling millions of requests per second with minimal GC pressure.
The framework gives you a clean separation of concerns. You decide which middleware to include and in what order. That's power — but also responsibility. A common mistake is placing exception handling after authentication: if auth throws, your custom handler never catches it. The framework swallows the exception and returns an empty 500 in production. No log, no trace. That's the kind of bug that wakes you up at 3 AM.
Routing and Controllers — Mapping URLs to Actions
Routing maps URLs to controller actions. For REST APIs, attribute routing keeps route definitions next to the action, so you can see endpoint shapes without jumping between files. Each controller is a class that groups related endpoints. Methods decorated with [HttpGet], [HttpPost], etc. become endpoints.
A subtle trace: route constraints like [HttpGet("{id:int}")] cause 404s if the client sends a non-integer. The framework treats constraint failure as route-not-matched, not validation failure. Many teams assume it returns 400, but it falls through to the next route. If no route matches, you get 404. You need a catch-all or manual validation in the action to get the expected 400.
Use [ApiController] on your controller class — it enables automatic model validation, binding source inference, and requires attribute routing. Without it, you have to specify [FromBody], [FromQuery], etc. explicitly.
- [Route("api/[controller]")] maps to the class name minus 'Controller' suffix.
- HTTP verb attributes (HttpGet, HttpPost) narrow the action selection.
- Combining both gives you RESTful CRUD endpoints.
- Always return ActionResult<T> to let ASP.NET Core handle status codes.
DTOs and Validation — Separating Internal Models from API Contracts
DTOs shape what your API sends and receives. They decouple your internal entity models from the public contract, so you can refactor internals without breaking clients. Use DTOs for both request and response — never expose EF Core entities directly.
ASP.NET Core integrates with Data Annotations for validation. Attributes like [Required], [StringLength], [Range] are evaluated automatically when binding from [FromBody]. With [ApiController], invalid DTOs return a 400 with standard ProblemDetails.
But here's where it gets you: [ApiController] only validates complex types bound from the body. Query parameters, route parameters, and headers are not automatically validated. You need to check ModelState manually or use FluentValidation with a custom filter. I've seen production data corrupted because a query param with an invalid type slipped through — the binding silently failed, and the parameter was null or default, not an error. Your code proceeds with bad input.
Global Error Handling — Don't Let Exceptions Leak to the Client
A production API must handle exceptions gracefully. Use DeveloperExceptionPage in development and a custom exception handler in production. The handler should log the exception and return a consistent ProblemDetails response with a correlation ID.
Write a custom middleware that catches all exceptions, logs them with request path and correlation ID, and returns a standard error object. Include a trace ID in every response, not just errors — then when a client complains about a slow response, you can trace it through logs using HttpContext.TraceIdentifier.
Register the handler first in the pipeline, before routing. If it's after routing, exceptions thrown during routing (e.g., missing route) bypass your handler. The fallback server error page in production returns an empty 500 with no details — that's the silent outage I described earlier.
- Register UseExceptionHandler before UseRouting, UseAuthentication, UseAuthorization.
- Include a correlation ID in the response so clients and logs can be matched.
- Use ProblemDetails format (RFC 7807) for all error responses.
- Log the full exception in the middleware, not just a message.
Dependency Injection — Loose Coupling for Testable APIs
DI is baked into ASP.NET Core. You register services in Program.cs with one of three lifetimes: Singleton (once per app start), Scoped (once per request), Transient (every request for the service). The container injects them wherever needed — typically via constructor injection in controllers.
Always program to interfaces. IProductService, IProductRepository — that way you can swap implementations for testing, caching, or fallback logic without changing the consumer. The built-in DI container is good enough for most apps; you rarely need third-party containers.
The trap: captive dependencies. If you register a Scoped service as Singleton, it works — but only one instance is created for the app's lifetime, reused across all requests. That means your DbContext holds stale data and causes concurrency errors. ASP.NET Core validates this in development and throws an exception, but in production it silently ignores the mismatch and works with the wrong behavior. Always verify your service lifetimes, especially when using custom factories.
Authorization: Don’t Let Your Endpoints Run Naked
Most junior devs treat authentication like a checkbox — slap JWT middleware on the pipeline and call it done. That’s how you get an API where every endpoint is a public bathroom door.
Authorization is the gatekeeper that says “you have a token? Cool. Does this token have the right claims to delete pizzas?”. In ASP.NET Core, you enforce this with [Authorize] attributes and policy-based checks. No attribute means open season for the entire internet.
Don’t handle JWT parsing yourself. Let ASP.NET’s built-in authentication middleware validate tokens, extract claims, and hydrate HttpContext.User. Then use [Authorize(Policy = “InventoryManager”)] to lock down write operations. That keeps your controller code clean and your security logic declarative.
One production leak I fixed: a contractor threw [AllowAnonymous] on a controller “for testing” and forgot to remove it. The Pizza catalog’s delete endpoint was wide open for two weeks. Policy-based authorization prevents that — you control access at the module level, not per-method guesswork.
[AllowAnonymous] at the controller level — put it only on the smallest set of endpoints that genuinely need public access. One catch-all override wipes out your entire auth fence.Serialization: Stop Writing Custom Mappers — JSON Is Not Your Enemy
I’ve seen teams write 200 lines of manual JsonSerializer calls because they “wanted control”. Every single time, the root cause was fear of a black-box framework. ASP.NET Core’s default JSON serialization via System.Text.Json is battle-tested and handles 99% of cases out of the box.
Here’s the deal: when you return an object from a controller action, ASP.NET serializes it to JSON automatically. No JsonConvert.SerializeObject, no custom ContentResult hacks. The framework respects your DTO shapes, ignores nulls if you configure it, and even handles enums as strings with a single option.
But the caveat? Circular references. If your EF models have navigation properties looping back to each other, the default serializer throws a JsonException. You have two choices: (1) use DTOs that flatten the graph (recommended), or (2) configure reference handling with ReferenceHandler.IgnoreCycles. The first option keeps your API contract clean; the second is a band-aid.
Real example: I inherited a pizza API where every endpoint returned {“$id”:”1″,”pizzas”:[{“$ref”:”1″}]} because someone enabled PreserveReferences globally. Customers got useless JSON. Fix: switch to DTO projection in the query and delete the config.
JsonSerializerOptions instance and pass it to JsonSerializer.Serialize() in integration tests. That way your test assertions match exactly what the production API returns — no brittleness from default config drift.Designed With Security In Mind — Stop Patching, Start Building
Security isn't a feature you bolt on after the demo. It's the foundation. ASP.NET Core gives you the weapons: built-in anti-forgery tokens, CSP headers, rate limiting middleware, and HTTPS enforcement out of the box.
Why does this matter? Because most breaches happen through exposed endpoints that trust the client too much. You validate input on every controller, you enforce HTTPS redirects globally, and you never, ever store secrets in appsettings.json — that's what User Secrets and Azure Key Vault are for.
How you implement it: start with the pipeline. Add app. before any route mapping. Use UseHttpsRedirection()[ValidateAntiForgeryToken] on state-changing actions. Lock down CORS to specific origins, not wildcards. And for the love of production, sanitize all user input even if you "trust" your frontend. You don't control the HTTP client calling your API, so treat every byte as hostile.
Senior move: Write a global authorization filter that rejects unauthenticated requests by default, then opt-in with [AllowAnonymous] only where needed.
Great Tools For Any Platform — Your API, Their Client
ASP.NET Core APIs don't care what language your client speaks. iOS, Android, React, Vue, even a curl script — the contract is JSON over HTTP. That's the point of REST: platform-agnostic communication.
Why should you care? Because you're not writing desktop apps anymore. Your API will be consumed by a web frontend, a mobile app, a partner system, and a CLI tool all at once. If you tie your API to a specific platform (like returning HTML views), you break that flexibility.
How to build for any platform: return proper HTTP status codes (201 for creation, 204 for deletion, 401 for unauthorized — no custom "success: false" garbage). Use content negotiation so clients can request JSON, XML, or even Protobuf. Document your API with OpenAPI/Swagger so any client team can self-serve. Don't expose your database schema — use DTOs that match what the client actually needs.
Senior move: Write a health endpoint (/health) that returns 200 with a JSON body. Every platform can hit it. If you ever change infrastructure, your clients won't break — they just check the status code.
The Missing Exception Handler That Swallowed Every Error
- Always place global error handling middleware first in the pipeline — before routing, authentication, or any other middleware.
- Test error responses with integration tests that simulate invalid requests.
- Enable server-side logging of unhandled exceptions using ILogger or a dedicated exception logging middleware.
UseMiniProfiler(). Check database connection pooling, inefficient LINQ queries, and synchronous I/O in async endpoints. Enable response compression for large payloads.dotnet run --launch-profile httpscurl -v https://localhost:5001/api/your-endpointKey takeaways
Common mistakes to avoid
7 patternsMemorising syntax before understanding the concept
Skipping practice and only reading theory
Not using dependency injection for services
Placing exception handling middleware in the wrong order
Exposing entity models directly in API responses
Assuming [ApiController] validates all parameters
Not returning ProblemDetails for error responses
Interview Questions on This Topic
Explain the role of the [ApiController] attribute and what it does automatically.
Frequently Asked Questions
20+ years shipping production .NET services in enterprise systems. Written from production experience, not tutorials.
That's ASP.NET. Mark it forged?
8 min read · try the examples if you haven't