Table of Contents

REST API

When you need a REST API for your SaaS

Not every product needs a public HTTP API. Add one when you want machine-to-machine access—for example mobile apps, partner integrations, automation, or a separate SPA that talks to your backend over JSON.

The SaaS template already includes an Api host (src/2-Clients/Api) wired with CodeBlock DevKit Web.Api and Web building blocks so you can add controllers next to the rest of your solution, reuse the same application services and use cases, and return a consistent Result / Result<T> shape to callers. For the REST API as a DevKit web client module (configuration, features, and module-specific notes), see REST API module introduction.

How feature endpoints fit the template

Typical flow:

  1. Controller (ASP.NET Core) — routes, [Authorize], DTO binding from body or query.
  2. Application service — your feature facade; builds commands/queries and returns Result (see Application services).
  3. Use cases — business workflow behind IRequestDispatcher (see Use cases).

Failures from validation, domain rules, or application rules are usually surfaced on Result.Errors without you writing try/catch in every action. For the full picture, see Exception handling.

Example controller: DemoThings

DemoThingsController.cs shows CRUD-style endpoints: route prefix, [Authorize(Policies.ADMIN_ROLE)], injection of IDemoThingService, and returning Task<Result<...>> directly from actions.

using CanBeYours.Application.Dtos.DemoThings;
using CanBeYours.Application.Services.DemoThings;
using CanBeYours.Core.Domain.DemoThings;
using CodeBlock.DevKit.Contracts.Dtos;
using CodeBlock.DevKit.Core.Helpers;
using CodeBlock.DevKit.Web.Api.Filters;
using CodeBlock.DevKit.Web.Security;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace CanBeYours.Api.Controllers;

/// <summary>
/// API controller for managing DemoThings - serves as an example of how to implement
/// CRUD operations in your own controllers. This controller demonstrates:
/// - Standard REST API patterns (GET, POST, PUT)
/// - Authorization using policies
/// - Input validation and DTOs
/// - Service layer integration
///
/// The current functionality is just for you to learn and understand how to implement
/// your own unique features into the current codebase.
/// </summary>
[Tags("DemoThings")]
[Route("demo-things")]
[Authorize(Policies.ADMIN_ROLE)]
public class DemoThingsController : BaseApiController
{
    private readonly IDemoThingService _demoThingService;

    /// <summary>
    /// Initializes a new instance of the DemoThingsController.
    /// </summary>
    /// <param name="demoThingService">Service for managing demo things</param>
    public DemoThingsController(IDemoThingService demoThingService)
    {
        _demoThingService = demoThingService;
    }

    /// <summary>
    /// Retrieves a demo thing by its unique identifier.
    /// Example: GET /demo-things/123
    /// </summary>
    /// <param name="id">Unique identifier of the demo thing</param>
    /// <returns>Demo thing data wrapped in a Result object</returns>
    [HttpGet]
    [Route("{id}")]
    public async Task<Result<GetDemoThingDto>> Get(string id)
    {
        return await _demoThingService.GetDemoThing(id);
    }

    /// <summary>
    /// Creates a new demo thing.
    /// Example: POST /demo-things with CreateDemoThingDto in body
    /// </summary>
    /// <param name="input">Data for creating the demo thing</param>
    /// <returns>Command result indicating success/failure</returns>
    [HttpPost]
    public async Task<Result<CommandResult>> Post([FromBody] CreateDemoThingDto input)
    {
        return await _demoThingService.CreateDemoThing(input);
    }

    /// <summary>
    /// Updates an existing demo thing by its identifier.
    /// Example: PUT /demo-things/123 with UpdateDemoThingDto in body
    /// </summary>
    /// <param name="id">Unique identifier of the demo thing to update</param>
    /// <param name="input">Updated data for the demo thing</param>
    /// <returns>Command result indicating success/failure</returns>
    [Route("{id}")]
    [HttpPut]
    public async Task<Result<CommandResult>> Put(string id, [FromBody] UpdateDemoThingDto input)
    {
        return await _demoThingService.UpdateDemoThing(id, input);
    }

    /// <summary>
    /// Searches for demo things with pagination, sorting, and filtering options.
    /// Example: GET /demo-things/1/10/asc?term=search&type=Type1&fromDateTime=2024-01-01
    /// </summary>
    /// <param name="pageNumber">Page number for pagination (1-based)</param>
    /// <param name="recordsPerPage">Number of records per page</param>
    /// <param name="sortOrder">Sort order (asc/desc)</param>
    /// <param name="term">Search term for filtering by name/description</param>
    /// <param name="type">Optional filter by demo thing type</param>
    /// <param name="fromDateTime">Optional start date filter</param>
    /// <param name="toDateTime">Optional end date filter</param>
    /// <returns>Paginated search results with demo things</returns>
    [HttpGet]
    [Route("{pageNumber}/{recordsPerPage}/{sortOrder}")]
    public async Task<Result<SearchOutputDto<GetDemoThingDto>>> Get(
        int pageNumber,
        int recordsPerPage,
        SortOrder sortOrder,
        [FromQuery] string term = null,
        [FromQuery] DemoThingType? type = null,
        [FromQuery] DateTime? fromDateTime = null,
        [FromQuery] DateTime? toDateTime = null
    )
    {
        var dto = new SearchDemoThingsInputDto
        {
            Term = term,
            PageNumber = pageNumber,
            RecordsPerPage = recordsPerPage,
            FromDateTime = fromDateTime,
            ToDateTime = toDateTime,
            SortOrder = sortOrder,
            Type = type,
        };
        return await _demoThingService.SearchDemoThings(dto);
    }
}

Copy this pattern for your feature: new controller, same BaseApiController base type, inject your IYourFeatureService, map HTTP verbs and parameters to service methods.

DevKit base controller and shared filters

All API controllers should inherit BaseApiController from CodeBlock.DevKit.Web.Api.Filters. It applies [ApiController], validates model state before actions run, and enables response-body logging for diagnostics.

The base type is defined in BaseApiController.cs.

// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

using CodeBlock.DevKit.Web.Filters;
using Microsoft.AspNetCore.Mvc;

namespace CodeBlock.DevKit.Web.Api.Filters;

/// <summary>
/// Base controller class for all API controllers in the application.
/// Provides common functionality including model state validation, response logging,
/// and client IP address detection. Inherit from this class to get standard API behavior.
/// </summary>
[ApiController]
[ModelStateValidating]
[HttpResponseLogging]
public class BaseApiController : ControllerBase
{
    /// <summary>
    /// Gets the client's IP address from the request.
    /// Checks for X-Forwarded-For header first (for proxy scenarios), then falls back to connection IP.
    /// </summary>
    /// <returns>The client's IP address as a string</returns>

    protected string GetClientIp()
    {
        string clientIp = HttpContext.Connection.RemoteIpAddress?.ToString();
        if (HttpContext.Request.Headers.ContainsKey("X-Forwarded-For"))
        {
            clientIp = HttpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault();
        }

        return clientIp;
    }
}
  • [ModelStateValidating] (from CodeBlock.DevKit.Web.Filters) — invalid model binding produces a Result.Failure-style response with validation messages instead of running the action blindly. Implemented in ModelStateValidatingAttribute.cs.
  • [HttpResponseLogging] (from CodeBlock.DevKit.Web.Api.Filters) — logs formatted response bodies for troubleshooting; respects DoNotLogResponseBodyAttribute on sensitive endpoints. Implemented in HttpResponseLoggingAttribute.cs.
  • GetClientIp() — helper that prefers X-Forwarded-For when your API sits behind a reverse proxy.

To skip logging request or response bodies on specific actions or controllers, use DoNotLogRequestBodyAttribute / DoNotLogResponseBodyAttribute in CodeBlock.DevKit.Web.Filters.

API-only middleware and infrastructure

Under CodeBlock.DevKit.Web.Api.Filters the DevKit also provides middleware you typically register in the API host startup (exact wiring lives with your host, but these are the pieces to be aware of):

Piece Role
ValidateApiKeyMiddleware Optional api-key header check for non-Swagger traffic when configured; skips Swagger and health paths.
LocalizationMiddleware Request localization for API responses.
SwaggerRootRedirectMiddleware Convenience redirect for Swagger UI root.

For unhandled exceptions that escape the normal MVC/MediatR pipeline, HttpGlobalExceptionHandler in CodeBlock.DevKit.Web.Api.Exceptions returns a safe, consistent HTTP error and logs the real failure.

// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

using System.Net;
using CodeBlock.DevKit.Core.Helpers;
using CodeBlock.DevKit.Core.Resources;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;

namespace CodeBlock.DevKit.Web.Api.Exceptions;

/// <summary>
/// Global exception handler middleware that catches unhandled exceptions.
/// Provides consistent error responses and logging for unexpected errors that occur during request processing.
/// </summary>
internal class HttpGlobalExceptionHandler : IMiddleware
{
    private readonly ILogger<HttpGlobalExceptionHandler> _logger;
    private readonly IStringLocalizer<CoreResource> _localizer;

    /// <summary>
    /// Initializes a new instance of the global exception handler.
    /// </summary>
    /// <param name="logger">Logger for recording error details</param>
    /// <param name="localizer">Localizer for providing localized error messages</param>
    public HttpGlobalExceptionHandler(ILogger<HttpGlobalExceptionHandler> logger, IStringLocalizer<CoreResource> localizer)
    {
        _logger = logger;
        _localizer = localizer;
    }

    /// <summary>
    /// Processes the HTTP request and catches any unhandled exceptions.
    /// Logs the exception and returns a standardized error response to the client.
    /// </summary>
    /// <param name="context">The HTTP context for the current request</param>
    /// <param name="next">The next middleware in the pipeline</param>
    /// <returns>A task representing the asynchronous operation</returns>

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        try
        {
            await next(context);
        }
        catch (Exception exception)
        {
            _logger.LogError(exception, "An unexpected error occured");
            await HandleExceptionAsync(context);
        }
    }

    /// <summary>
    /// Handles the caught exception by setting appropriate response properties.
    /// Returns a standardized error response with localized error message.
    /// </summary>
    /// <param name="httpContext">The HTTP context to write the error response to</param>
    /// <returns>A task representing the asynchronous operation</returns>
    private async Task HandleExceptionAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = "application/json";
        httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        var result = Result.Failure(message: _localizer[CoreResource.Unknown_Exception_Error]);
        await httpContext.Response.WriteAsJsonAsync(result);
    }
}

Authorization and rate limiting

Authorization policies (roles, permissions, user types) are defined through Policies and related helpers in CodeBlock.DevKit.Web.Security. Your controllers use [Authorize(PolicyName)] with constants such as Policies.ADMIN_ROLE, matching how the template secures admin-only APIs.

Rate limiting uses ASP.NET Core’s rate limiter integration; DevKit exposes GlobalFixedRateLimiter (GLOBAL_FIXED_RATE_LIMITER_POLICY) so hosts can apply a global fixed window. Opt out per endpoint with [DisableRateLimiting] when an action must stay unlimited.

The DevKit sample API demonstrates both patterns in TestController.cs.

// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

using CodeBlock.DevKit.Core.Helpers;
using CodeBlock.DevKit.Web.Api.Filters;
using CodeBlock.DevKit.Web.Security;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;

namespace Api.Controllers;

[Route("test")]
public class TestController : BaseApiController
{
    /// <summary>
    /// This api is protected by authorization
    /// </summary>
    [Authorize(Policies.ADMIN_ROLE)]
    [HttpGet]
    [Route("authorized")]
    public async Task<Result> Authorized()
    {
        return Result.Success();
    }

    /// <summary>
    /// This api has no rate limit
    /// </summary>
    [HttpGet]
    [Route("unlimited")]
    [DisableRateLimiting]
    public async Task<Result> Unlimited()
    {
        return Result.Success();
    }

    /// <summary>
    /// This api is proteted by rate limit
    /// </summary>
    [HttpGet]
    [Route("limited")]
    public async Task<Result> Limited()
    {
        return Result.Success();
    }
}

For more security-focused options (including rate limiting configuration in product docs), see the Security module introduction.