Controllers & Actions

Mapping every route inline in Program.cs works for a handful of endpoints, but real APIs organize related endpoints into controllers — classes where each method is an action handling one route.

Defining a controller

A controller is a class inheriting from ControllerBase, decorated with attributes that set its base route and mark it as an API controller:

C# Controllers/ProductsController.cs
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAll()
    {
        var products = new[] { "Keyboard", "Mouse", "Monitor" };
        return Ok(products);
    }
}
Terminal — GET /api/products
$ curl http://localhost:5225/api/products
["Keyboard","Mouse","Monitor"]

[Route("api/[controller]")] uses the controller's name (minus "Controller") to build the base path — ProductsController becomes api/products automatically. [HttpGet] on the method maps it to GET requests on that base route. Ok(products) returns a 200 response with the value serialized to JSON.

Action methods with parameters

A route parameter in the method's own [HttpGet] attribute appends to the controller's base route:

C# Controllers/ProductsController.cs
[HttpGet("{id:int}")]
public IActionResult GetById(int id)
{
    if (id > 100)
        return NotFound();

    return Ok($"Product #{id}");
}
Terminal
$ curl -i http://localhost:5225/api/products/5
HTTP/1.1 200 OK
"Product #5"

$ curl -i http://localhost:5225/api/products/999
HTTP/1.1 404 Not Found

NotFound() and Ok(...) are both helper methods on ControllerBase that build a proper IActionResult with the right status code — you're never manually setting Response.StatusCode for common cases like these.

Note: [ApiController] isn't just decoration — it turns on automatic model-validation responses (a 400 with error details when binding fails, covered in the next lesson) and a few other API-specific conventions. Leaving it off a controller that's meant to be an API silently disables that behavior.