Skip to main content

API Controller

What is [ApiController]?

[ApiController] is an attribute you apply to controller classes to enable API-specific behaviors and conventions in ASP.NET Core. Think of it as "API mode" for your controllers - it automatically handles many common API tasks that you'd otherwise have to code manually.

Here's a typical use:

[Route("api/[controller]")]
[ApiController] // This attribute right here
public class ProductsController : ControllerBase
{
// Your API actions
}

That single attribute gives you a collection of automatic behaviors that make building APIs easier, cleaner, and more consistent.

What Does It Do?

When you decorate a controller with [ApiController], you get these automatic behaviors:

1. Automatic HTTP 400 Responses

Without [ApiController]:

You have to manually check if the model state is valid and return appropriate errors:

public class ProductsController : ControllerBase
{
[HttpPost]
public ActionResult<Product> CreateProduct(Product product)
{
// You must manually check this
if (!ModelState.IsValid)
{
return BadRequest(ModelState); // Manual error handling
}

// Your actual logic
return Ok(product);
}
}

With [ApiController]:

Model validation happens automatically - no manual checks needed:

[ApiController]
public class ProductsController : ControllerBase
{
[HttpPost]
public ActionResult<Product> CreateProduct(Product product)
{
// ModelState is automatically checked
// If invalid, 400 BadRequest is returned automatically
// You never get here with invalid data

// Your actual logic - assume product is valid
return Ok(product);
}
}

If a client sends invalid data, they automatically get a 400 response with details about what went wrong:

{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Name": [
"The Name field is required."
],
"Price": [
"The field Price must be between 0 and 10000."
]
}
}

2. Binding Source Parameter Inference

Without [ApiController]:

You must explicitly tell ASP.NET Core where each parameter comes from:

public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
public ActionResult<Product> GetProduct([FromRoute] Guid id) // Must specify
{
// ...
}

[HttpPost]
public ActionResult<Product> CreateProduct([FromBody] Product product) // Must specify
{
// ...
}

[HttpGet]
public ActionResult<IEnumerable<Product>> Search([FromQuery] string name) // Must specify
{
// ...
}
}

With [ApiController]:

The attribute infers where parameters come from based on their type and position:

[ApiController]
public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
public ActionResult<Product> GetProduct(Guid id) // Automatically [FromRoute]
{
// ...
}

[HttpPost]
public ActionResult<Product> CreateProduct(Product product) // Automatically [FromBody]
{
// ...
}

[HttpGet]
public ActionResult<IEnumerable<Product>> Search(string name) // Automatically [FromQuery]
{
// ...
}
}

The inference rules:

  • Complex types (classes, objects) → [FromBody]
  • Simple types (string, int, Guid, etc.) in route template → [FromRoute]
  • Simple types not in route template → [FromQuery]
  • IFormFile and IFormFileCollection[FromForm]

3. Multipart/Form-Data Request Inference

When you use IFormFile or IFormFileCollection, the attribute automatically infers [FromForm]:

[ApiController]
public class FilesController : ControllerBase
{
[HttpPost("upload")]
public ActionResult UploadFile(IFormFile file) // Automatically [FromForm]
{
// Handle file upload
return Ok();
}
}

4. Problem Details for Error Status Codes

When you return error status codes (4xx and 5xx), the response is automatically wrapped in a standardized problem details format (RFC 7807):

[ApiController]
public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
public ActionResult<Product> GetProduct(Guid id)
{
var product = _repository.GetById(id);

if (product == null)
{
return NotFound(); // Returns standardized problem details
}

return Ok(product);
}
}

Response for NotFound():

{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
"title": "Not Found",
"status": 404,
"traceId": "00-abc123..."
}

Why Use [ApiController]?

1. Less Boilerplate Code

Compare these two approaches for a simple POST action:

Without [ApiController] (manual everything):

[Route("api/products")]
public class ProductsController : ControllerBase
{
[HttpPost]
public ActionResult<Product> CreateProduct([FromBody] Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

if (product == null)
{
return BadRequest("Product cannot be null");
}

// Actual logic
_repository.Add(product);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
}

With [ApiController] (automatic behaviors):

[Route("api/products")]
[ApiController]
public class ProductsController : ControllerBase
{
[HttpPost]
public ActionResult<Product> CreateProduct(Product product)
{
// Validation already happened, product is guaranteed valid
// Just write your business logic
_repository.Add(product);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
}

2. Consistent Error Responses

All validation errors follow the same format across your entire API - no need to manually format error responses.

3. Convention Over Configuration

The attribute embraces conventions - simple types in routes come from the route, complex types come from the body. You write less code and the behavior is predictable.

Requirements for [ApiController]

To use [ApiController], your controller must:

1. Inherit from ControllerBase

// ✅ Good - inherits from ControllerBase
[ApiController]
public class ProductsController : ControllerBase
{
// ...
}

// ❌ Bad - doesn't inherit from ControllerBase
[ApiController]
public class ProductsController
{
// Won't work
}

2. Have a Route Attribute

You must specify a route on the controller or action:

// ✅ Good - has route attribute
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
// ...
}

// ❌ Bad - no route attribute
[ApiController]
public class ProductsController : ControllerBase
{
// Won't work properly
}

Applying to Multiple Controllers

You can apply [ApiController] to individual controllers, or to a base class that all your API controllers inherit from:

Individual Controllers

[Route("api/products")]
[ApiController]
public class ProductsController : ControllerBase { }

[Route("api/orders")]
[ApiController]
public class OrdersController : ControllerBase { }

[Route("api/customers")]
[ApiController]
public class CustomersController : ControllerBase { }

Base Controller

[ApiController]
public class ApiControllerBase : ControllerBase
{
// Common functionality for all API controllers
}

[Route("api/products")]
public class ProductsController : ApiControllerBase { }

[Route("api/orders")]
public class OrdersController : ApiControllerBase { }

[Route("api/customers")]
public class CustomersController : ApiControllerBase { }

Assembly-Level

Apply to all controllers in the assembly:

// In Program.cs or a separate file
[assembly: ApiController]

// Now all controllers automatically get the behavior
[Route("api/products")]
public class ProductsController : ControllerBase { }

Customizing Behavior

You can customize how [ApiController] behaves:

Custom Validation Response

In Program.cs, configure how validation errors are formatted:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var errors = context.ModelState
.Where(e => e.Value.Errors.Count > 0)
.Select(e => new
{
Name = e.Key,
Message = e.Value.Errors.First().ErrorMessage
})
.ToArray();

return new BadRequestObjectResult(new
{
Message = "Validation failed",
Errors = errors
});
};
});

Now your validation errors look like:

{
"message": "Validation failed",
"errors": [
{
"name": "Name",
"message": "The Name field is required."
},
{
"name": "Price",
"message": "The field Price must be between 0 and 10000."
}
]
}

Disable Automatic 400 Responses

If you want to manually handle validation:

builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressModelStateInvalidFilter = true;
});

Now you're back to manually checking ModelState.IsValid.

Disable Binding Source Inference

If you want to explicitly specify [FromBody], [FromRoute], etc.:

builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressInferBindingSourcesForParameters = true;
});

Real-World Example

Here's a complete controller showing [ApiController] in action:

using Microsoft.AspNetCore.Mvc;

namespace ProductAPI.Controllers;

[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
private readonly IProductRepository _repository;
private readonly ILogger<ProductsController> _logger;

public ProductsController(
IProductRepository repository,
ILogger<ProductsController> logger)
{
_repository = repository;
_logger = logger;
}

// GET: api/products
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetAll()
{
var products = await _repository.GetAllAsync();
return Ok(products);
}

// GET: api/products/{id}
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetById(Guid id) // id automatically from route
{
var product = await _repository.GetByIdAsync(id);

if (product == null)
{
return NotFound(); // Automatic problem details response
}

return Ok(product);
}

// POST: api/products
[HttpPost]
public async Task<ActionResult<Product>> Create(Product product) // product automatically from body
{
// No need to check ModelState - already validated automatically
// If validation failed, this code never runs

await _repository.AddAsync(product);

return CreatedAtAction(
nameof(GetById),
new { id = product.Id },
product);
}

// PUT: api/products/{id}
[HttpPut("{id}")]
public async Task<ActionResult> Update(Guid id, Product product)
{
// id from route, product from body - both automatic

if (id != product.Id)
{
return BadRequest("ID mismatch");
}

var exists = await _repository.ExistsAsync(id);
if (!exists)
{
return NotFound();
}

await _repository.UpdateAsync(product);
return NoContent();
}

// DELETE: api/products/{id}
[HttpDelete("{id}")]
public async Task<ActionResult> Delete(Guid id)
{
var product = await _repository.GetByIdAsync(id);

if (product == null)
{
return NotFound();
}

await _repository.DeleteAsync(id);
return NoContent();
}

// GET: api/products/search?name=laptop&minPrice=100
[HttpGet("search")]
public async Task<ActionResult<IEnumerable<Product>>> Search(
string name, // Automatically from query string
decimal? minPrice // Automatically from query string
)
{
var products = await _repository.SearchAsync(name, minPrice);
return Ok(products);
}
}

Notice how clean this is:

  • No manual ModelState.IsValid checks
  • No [FromRoute], [FromBody], [FromQuery] attributes needed
  • Automatic problem details for errors
  • All validation happens before your code runs

When NOT to Use [ApiController]

There are rare cases where you might not want it:

1. MVC Controllers (Views)

If you're returning views (HTML), don't use [ApiController]:

// This is an MVC controller, not an API controller
public class HomeController : Controller
{
public IActionResult Index()
{
return View(); // Returns HTML
}
}

2. Need Manual Control

If you need complete control over validation and error responses:

public class CustomController : ControllerBase
{
[HttpPost]
public ActionResult Create(Product product)
{
// Custom validation logic
if (!IsValid(product))
{
return BadRequest(new CustomErrorFormat
{
// Your specific error format
});
}

// ...
}
}

But honestly, these cases are rare. 99% of the time, [ApiController] is exactly what you want.

Wrap Up

The [ApiController] attribute is one of those features that just makes sense - it automates the boring, repetitive parts of building APIs while giving you a consistent, predictable behavior.

Key benefits:

  • Automatic model validation - no manual ModelState.IsValid checks
  • Automatic parameter binding - infers [FromBody], [FromRoute], [FromQuery]
  • Standardized error responses - consistent problem details format
  • Less boilerplate - more time writing business logic, less time on plumbing

When building ASP.NET Core APIs, just use [ApiController] on all your controllers. It's a modern best practice and makes your code cleaner and more maintainable. The behaviors it provides are exactly what you'd want anyway - it just does them automatically.