examples/Todo is a small CRUD service for a Todo resource. It pulls together most of the building blocks a real Baldr app uses: a singleton repository registered through DI, a controller that groups its routes under /api/todos, bound path / body / query parameters via baldr::FromParams / baldr::FromBody / baldr::FromQuery, validation errors expressed as typed results, and an OpenAPI 3.0.3 spec plus Scalar UI.
#include"TodoController.hpp"#include<Baldr/Http/FromBody.hpp>#include<Baldr/Http/FromParams.hpp>#include<Baldr/Http/FromQuery.hpp>#include<Baldr/Http/Results/Result.hpp>#include<Baldr/Http/Results/TypedResults.hpp>#include<utility>#include<variant>TodoController::TodoController(skr::Arc<ITodoRepository>repository):mRepository(std::move(repository)){}structValidationError{std::stringfield;std::stringmessage;};voidTodoController::Register(baldr::WebApplication&app){app.MapGroup("/api/todos",[this](auto&group){group.MapGet("/").WithSummary("List todos (paged)").Handle([this](baldr::FromQuery<PageQuery>q)->std::variant<baldr::JsonResult<ValidationError,baldr::StatusCode::BadRequest>,baldr::JsonResult<TodoPage,baldr::StatusCode::OK>>{if(!q.isOk())returnbaldr::Results::Json<ValidationError,baldr::StatusCode::BadRequest>(ValidationError{"query",q.error->message});auton=q.value.normalized();autoitems=mRepository->List(n.pageSize,n.offset);autototal=mRepository->Count();returnbaldr::Results::Json<TodoPage,baldr::StatusCode::OK>(TodoPage{.items=std::move(items),.page=n.page,.pageSize=n.pageSize,.total=total});});group.MapGet("/:id").WithSummary("Get a todo by id").Handle([this](baldr::FromParams<IdParam>params)->std::variant<baldr::JsonResult<Todo,baldr::StatusCode::OK>,baldr::NotFoundResult>{autofound=mRepository->GetById(params.value.id);if(!found)returnbaldr::Results::NotFound();returnbaldr::Results::Json<Todo,baldr::StatusCode::OK>(*found);});group.MapPost("/").WithSummary("Create a todo").Handle([this](baldr::FromBody<CreateTodoDto>body)->std::variant<baldr::JsonResult<Todo,baldr::StatusCode::Created>,baldr::JsonResult<ValidationError,baldr::StatusCode::BadRequest>>{if(body.value.title.empty())returnbaldr::Results::Json<ValidationError,baldr::StatusCode::BadRequest>(ValidationError{"title","title is required"});autotodo=mRepository->Create(std::move(body.value.title),body.value.done);returnbaldr::Results::Json<Todo,baldr::StatusCode::Created>(std::move(todo));});group.MapPut("/:id").WithSummary("Update a todo").Handle([this](baldr::FromParams<IdParam>params,baldr::FromBody<UpdateTodoDto>body)->std::variant<baldr::JsonResult<Todo,baldr::StatusCode::OK>,baldr::JsonResult<ValidationError,baldr::StatusCode::BadRequest>,baldr::NotFoundResult>{if(body.value.title.empty())returnbaldr::JsonResult<ValidationError,baldr::StatusCode::BadRequest>(ValidationError{"title","title is required"});autoupdated=mRepository->Update(params.value.id,std::move(body.value.title),body.value.done);if(!updated)returnbaldr::Results::NotFound();returnbaldr::Results::Json<Todo,baldr::StatusCode::OK>(*updated);});group.MapDelete("/:id").WithSummary("Delete a todo").Handle([this](baldr::FromParams<IdParam>params)->std::variant<baldr::NotFoundResult,baldr::NoContentResult>{if(!mRepository->Delete(params.value.id))returnbaldr::Results::NotFound();returnbaldr::Results::NoContent();});});}
Registering an interface-to-implementation binding with AddSingleton<ITodoRepository, InMemoryTodoRepository>() so the rest of the app can depend on the abstraction.
Resolving a singleton from the root service provider in main and passing it into a controller manually (instead of taking it as a route-handler parameter).
Grouping routes under a common prefix with app.MapGroup("/api/todos", [](auto& group) { ... }).
Binding typed path, body, and query parameters with baldr::FromParams<T>, baldr::FromBody<T>, and baldr::FromQuery<T>, where the wrapped type aggregates the route params, JSON body, or query string.
Modelling multiple success and error outcomes from a single handler with std::variant of JsonResult<T, Status> and result markers such as NotFoundResult / NoContentResult.
Returning a ValidationError DTO under 400 Bad Request from POST, PUT, and paged GET when the input fails to bind — distinct from a bare BadRequestResult.
Paginating the list endpoint with FromQuery<PageQuery> (pageSize clamped to [1, 500], page clamped to >= 1) and returning a TodoPage envelope carrying items, page, pageSize, and total so clients can detect when more data is available.
Wiring the OpenAPI extension and the Scalar UI so the controller's routes appear in the generated spec at /openapi.json (query parameters included automatically) and the UI at /scalar.
# List (empty at first; defaults: page=1, pageSize=500)
curlhttp://localhost:8080/api/todos/
# Create
curl-i-XPOSThttp://localhost:8080/api/todos/\-H'Content-Type: application/json'\-d'{"title":"Write docs","done":false}'# Get by id
curlhttp://localhost:8080/api/todos/1
# Update
curl-XPUThttp://localhost:8080/api/todos/1\-H'Content-Type: application/json'\-d'{"title":"Write docs","done":true}'# Delete
curl-i-XDELETEhttp://localhost:8080/api/todos/1
# Pagination — page 1 of 10, then page 2
curl'http://localhost:8080/api/todos/?page=1&pageSize=10'
curl'http://localhost:8080/api/todos/?page=2&pageSize=10'# Validation error (missing field on POST)
curl-i-XPOSThttp://localhost:8080/api/todos/\-H'Content-Type: application/json'\-d'{"title":"","done":false}'# Validation error (malformed query value)
curl-i'http://localhost:8080/api/todos/?pageSize=abc'# Browse the spec and UI
curlhttp://localhost:8080/openapi.json|jq'.paths, .components.schemas'# Scalar UI is served at /scalar