Skip to main content

Creating API Controllers

Controllers expose your use cases through HTTP endpoints. This architecture provides a standardized approach to API responses and error handling.

Overview

Every controller must:
  • Inherit from BaseController
  • Use dependency injection for ICommandQueryBus
  • Follow RESTful conventions
  • Return standardized responses

Step-by-Step Guide

1

Create the Controller Class

Create a new controller in Template-API/Controllers/YourEntityController.cs:
using Application.UseCases.YourEntity.Commands.CreateYourEntity;
using Application.UseCases.YourEntity.Commands.UpdateYourEntity;
using Application.UseCases.YourEntity.Commands.DeleteYourEntity;
using Application.UseCases.YourEntity.Queries.GetAllYourEntities;
using Application.UseCases.YourEntity.Queries.GetYourEntityBy;
using Core.Application;
using Microsoft.AspNetCore.Mvc;

namespace Controllers
{
    /// <summary>
    /// API controller for YourEntity CRUD operations
    /// Inherits from BaseController for standardized responses
    /// </summary>
    [ApiController]
    public class YourEntityController : BaseController
    {
        private readonly ICommandQueryBus _commandQueryBus;

        public YourEntityController(ICommandQueryBus commandQueryBus)
        {
            _commandQueryBus = commandQueryBus ?? throw new ArgumentNullException(nameof(commandQueryBus));
        }

        // Endpoints will go here
    }
}
Example from codebase: Template-API/Controllers/DummyEntityController.cs:19-21
2

Implement GET All Endpoint

Add a GET endpoint with pagination support:
[HttpGet("api/v1/[Controller]")]
public async Task<IActionResult> GetAll(uint pageIndex = 1, uint pageSize = 10)
{
    var query = new GetAllYourEntitiesQuery
    {
        PageIndex = pageIndex,
        PageSize = pageSize
    };

    var entities = await _commandQueryBus.Send(query);

    return Ok(entities);
}
Example from codebase: Template-API/Controllers/DummyEntityController.cs:23-29Request:
GET /api/v1/YourEntity?pageIndex=1&pageSize=10
Response:
{
  "success": true,
  "data": {
    "items": [...],
    "pageIndex": 1,
    "pageSize": 10,
    "totalCount": 100
  },
  "message": "",
  "code": "",
  "statusCode": 200
}
3

Implement GET by ID Endpoint

Add a GET endpoint for retrieving a single entity:
[HttpGet("api/v1/[Controller]/{id}")]
public async Task<IActionResult> GetById(string id)
{
    if (string.IsNullOrEmpty(id))
        return BadRequest();

    var query = new GetYourEntityByQuery { Id = id };
    var entity = await _commandQueryBus.Send(query);

    return Ok(entity);
}
Example from codebase: Template-API/Controllers/DummyEntityController.cs:31-39Request:
GET /api/v1/YourEntity/123e4567-e89b-12d3-a456-426614174000
Response:
{
  "success": true,
  "data": {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "propertyOne": "value",
    "propertyTwo": 42
  },
  "message": "",
  "code": "",
  "statusCode": 200
}
4

Implement POST Endpoint

Add a POST endpoint for creating entities:
[HttpPost("api/v1/[Controller]")]
public async Task<IActionResult> Create(CreateYourEntityCommand command)
{
    if (command is null)
        return BadRequest();

    var id = await _commandQueryBus.Send(command);

    return Created($"api/[Controller]/{id}", new { Id = id });
}
Example from codebase: Template-API/Controllers/DummyEntityController.cs:41-49Request:
POST /api/v1/YourEntity
Content-Type: application/json

{
  "propertyOne": "value",
  "propertyTwo": 42
}
Response:
{
  "id": "123e4567-e89b-12d3-a456-426614174000"
}
Status: 201 CreatedLocation Header: api/YourEntity/123e4567-e89b-12d3-a456-426614174000
5

Implement PUT Endpoint

Add a PUT endpoint for updating entities:
[HttpPut("api/v1/[Controller]")]
public async Task<IActionResult> Update(UpdateYourEntityCommand command)
{
    if (command is null)
        return BadRequest();

    await _commandQueryBus.Send(command);

    return NoContent();
}
Example from codebase: Template-API/Controllers/DummyEntityController.cs:51-59Request:
PUT /api/v1/YourEntity
Content-Type: application/json

{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "propertyOne": "updated value",
  "propertyTwo": 100
}
Response: Empty bodyStatus: 204 No Content
6

Implement DELETE Endpoint

Add a DELETE endpoint for removing entities:
[HttpDelete("api/v1/[Controller]/{id}")]
public async Task<IActionResult> Delete(string id)
{
    if (string.IsNullOrEmpty(id))
        return BadRequest();

    await _commandQueryBus.Send(new DeleteYourEntityCommand { Id = id });

    return NoContent();
}
Example from codebase: Template-API/Controllers/DummyEntityController.cs:61-69Request:
DELETE /api/v1/YourEntity/123e4567-e89b-12d3-a456-426614174000
Response: Empty bodyStatus: 204 No Content

BaseController Features

The BaseController provides standardized response formatting.

Standard Response Format

All successful responses are wrapped in a standard format:
public class HttpMessageResult
{
    public bool Success { set; get; }      // true for successful operations
    public object Data { set; get; }       // The actual response data
    public string Message { set; get; }    // Error message if Success is false
    public string Code { set; get; }       // Error code if Success is false
    public int StatusCode { set; get; }    // HTTP status code
}
Reference: Template-API/Controllers/BaseController.cs:30-37

Automatic Response Wrapping

When you call Ok(data), the response is automatically wrapped:
// Your code
return Ok(entity);

// Actual response
{
  "success": true,
  "data": { ...entity... },
  "message": "",
  "code": "",
  "statusCode": 200
}
Reference: Template-API/Controllers/BaseController.cs:12-27

Error Handling with BaseExceptionFilter

All exceptions are automatically handled by BaseExceptionFilter.

Exception Mapping

// Domain exceptions are mapped to HTTP status codes:
EntityDoesNotExistException404 Not Found
EntityDoesExistException400 Bad Request
InvalidEntityDataException400 Bad Request
BussinessException400 Bad Request
ArgumentNullException411 Length Required
Unknown exceptions500 Internal Server Error
Reference: Template-API/Filters/BaseExceptionFilter.cs:34-60

Error Response Format

{
  "success": false,
  "data": "",
  "message": "Entity with ID 'xyz' does not exist",
  "code": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "statusCode": 404
}

Registering the Filter

The filter is registered in Startup.cs:
services.AddMvc().AddMvcOptions(options =>
{
    options.Filters.Add<BaseExceptionFilter>();
});
Reference: Template-API/Startup.cs:29-32

Complete Controller Example

Here’s the complete DummyEntityController from the codebase:
using Application.UseCases.DummyEntity.Commands.CreateDummyEntity;
using Application.UseCases.DummyEntity.Commands.DeleteDummyEntity;
using Application.UseCases.DummyEntity.Commands.UpdateDummyEntity;
using Application.UseCases.DummyEntity.Queries.GetAllDummyEntities;
using Application.UseCases.DummyEntity.Queries.GetDummyEntityBy;
using Core.Application;
using Microsoft.AspNetCore.Mvc;

namespace Controllers
{
    [ApiController]
    public class DummyEntityController : BaseController
    {
        private readonly ICommandQueryBus _commandQueryBus;

        public DummyEntityController(ICommandQueryBus commandQueryBus)
        {
            _commandQueryBus = commandQueryBus ?? throw new ArgumentNullException(nameof(commandQueryBus));
        }

        [HttpGet("api/v1/[Controller]")]
        public async Task<IActionResult> GetAll(uint pageIndex = 1, uint pageSize = 10)
        {
            var entities = await _commandQueryBus.Send(new GetAllDummyEntitiesQuery() { PageIndex = pageIndex, PageSize = pageSize });
            return Ok(entities);
        }

        [HttpGet("api/v1/[Controller]/{id}")]
        public async Task<IActionResult> GetById(string id)
        {
            if (string.IsNullOrEmpty(id)) return BadRequest();
            var entity = await _commandQueryBus.Send(new GetDummyEntityByQuery { DummyIdProperty = id });
            return Ok(entity);
        }

        [HttpPost("api/v1/[Controller]")]
        public async Task<IActionResult> Create(CreateDummyEntityCommand command)
        {
            if (command is null) return BadRequest();
            var id = await _commandQueryBus.Send(command);
            return Created($"api/[Controller]/{id}", new { Id = id });
        }

        [HttpPut("api/v1/[Controller]")]
        public async Task<IActionResult> Update(UpdateDummyEntityCommand command)
        {
            if (command is null) return BadRequest();
            await _commandQueryBus.Send(command);
            return NoContent();
        }

        [HttpDelete("api/v1/[Controller]/{id}")]
        public async Task<IActionResult> Delete(int id)
        {
            if (id <= 0) return BadRequest();
            await _commandQueryBus.Send(new DeleteDummyEntityCommand { DummyIdProperty = id });
            return NoContent();
        }
    }
}
Reference: Template-API/Controllers/DummyEntityController.cs

Best Practices

Follow these conventions:
  • GET /api/v1/[Controller] - List all (with pagination)
  • GET /api/v1/[Controller]/{id} - Get single
  • POST /api/v1/[Controller] - Create
  • PUT /api/v1/[Controller] - Update
  • DELETE /api/v1/[Controller]/{id} - Delete
Use [Controller] token for automatic controller name replacement.
Perform basic validation in the controller:
if (command is null)
    return BadRequest();

if (string.IsNullOrEmpty(id))
    return BadRequest();
Domain validation is handled by entity validators and caught by BaseExceptionFilter.
Use appropriate HTTP status codes:
  • Ok(data) - 200 OK (GET, successful query)
  • Created(location, data) - 201 Created (POST)
  • NoContent() - 204 No Content (PUT, DELETE)
  • BadRequest() - 400 Bad Request (validation failure)
  • Exceptions are mapped automatically by the filter
Always use async/await for database operations:
public async Task<IActionResult> GetById(string id)
{
    var entity = await _commandQueryBus.Send(query);
    return Ok(entity);
}

Testing Your API

Using Swagger

The API includes Swagger UI for testing:
  1. Run the application
  2. Navigate to https://localhost:<port>/swagger
  3. Test endpoints interactively

Using cURL

# Create entity
curl -X POST https://localhost:5001/api/v1/YourEntity \
  -H "Content-Type: application/json" \
  -d '{"propertyOne":"test","propertyTwo":42}'

# Get entity
curl https://localhost:5001/api/v1/YourEntity/{id}

# Update entity
curl -X PUT https://localhost:5001/api/v1/YourEntity \
  -H "Content-Type: application/json" \
  -d '{"id":"...","propertyOne":"updated","propertyTwo":100}'

# Delete entity
curl -X DELETE https://localhost:5001/api/v1/YourEntity/{id}

Next Steps

Database Setup

Configure database connections and migrations

Docker Deployment

Deploy your API using Docker

Build docs developers (and LLMs) love