REST API ASP.NET Core — Silent 500s from Middleware Order
All endpoints returned 500 with empty body in production because UseExceptionHandler after UseRouting.
- 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.
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.Key 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
That's ASP.NET. Mark it forged?
4 min read · try the examples if you haven't