VSATutorialSeptember 2026 · 7 min read

VSA Todo App — Part 5: Minimal API Endpoints & Route Organization

TL;DR

Part 5 organizes VSA endpoints using Minimal API MapGroup for route prefixes, JWT authorization at the group level, request size limits for file uploads, separate endpoints for child data (items, attachments, downloads), and the hybrid pattern of MVC controllers coexisting with Minimal APIs in the same feature slice.

Part 5 of 20 in the VSA Todo App Tutorial Series | ← Previous: Part 4|Next: Part 6 →

Help Us Grow

Love this tutorial? Explore our ready-to-use enterprise starter kits built with VSA in .NET 10.

Endpoints are the entry points to your VSA feature slices. In VSA, each feature owns its endpoints — they live in the feature folder, not in a global Program.cs or a shared routes file. This means adding a feature adds endpoints, and removing a feature removes them cleanly. Both the Blazor CRM and MVC Project Manager follow this pattern, though they take different approaches to endpoint registration.

The Blazor CRM uses pure Minimal APIs with MapGroup for route organization. The MVC Project Manager uses a hybrid approach: MVC controllers for Razor views plus Minimal APIs for REST endpoints. Both patterns coexist in the same feature folder, and both demonstrate how VSA keeps endpoint definitions close to their handlers.

MapGroup: One Route Prefix for the Entire Feature

The MapGroup method is the backbone of VSA endpoint organization. It creates a route group with a shared prefix, shared authorization, and shared OpenAPI tags. The Blazor CRM Todo endpoint uses it to group all Todo routes under /api/todo:

public static class TodoEndpoint
{
    public static void MapTodoEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/todo")
            .WithTags("Todos")
            .RequireAuthorization(policy => policy
                .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
                .RequireAuthenticatedUser());

        group.MapGet("/", async (IMediator mediator) =>
        {
            var result = await mediator.Send(new GetTodoListQuery());
            return Results.Ok(result);
        }).WithName("GetTodoList");

        group.MapGet("/{id}", async (string id, IMediator mediator) =>
        {
            var result = await mediator.Send(new GetTodoByIdQuery(id));
            return result is not null ? Results.Ok(result) : Results.NotFound();
        }).WithName("GetTodoById");

        group.MapPost("/", async (CreateTodoRequest request, IMediator mediator) =>
        {
            var result = await mediator.Send(new CreateTodoCommand(request));
            return Results.Created($"/api/todo/{result.Id}", result);
        }).WithName("CreateTodo");

        group.MapPost("/update", async (UpdateTodoRequest request, IMediator mediator) =>
        {
            var result = await mediator.Send(new UpdateTodoCommand(request));
            return result.Success ? Results.Ok(result) : Results.NotFound();
        }).WithName("UpdateTodo");

        group.MapPost("/delete/{id}", async (string id, IMediator mediator) =>
        {
            var result = await mediator.Send(new DeleteTodoByIdCommand(
                new DeleteTodoByIdRequest(id)));
            return result ? Results.Ok() : Results.NotFound();
        }).WithName("DeleteTodoById");
    }
}

Every endpoint under this group inherits JWT authentication automatically — no need to repeat [Authorize] on each route. The group also sets the OpenAPI tag to "Todos" so Swagger groups them together. Registration is a single line in Program.cs: app.MapTodoEndpoints().

Child Data Endpoints: Separate Routes for Items and Attachments

The MVC Project Manager goes further by exposing child data through dedicated endpoints. This allows the frontend to fetch items, image attachments, and file attachments independently without loading the entire Todo:

group.MapGet("/{id}/items", async (string id, IMediator mediator) =>
{
    var todo = await mediator.Send(new GetTodoByIdQuery(id));
    return todo is not null ? Results.Ok(todo.TodoItems) : Results.NotFound();
});

group.MapGet("/{id}/image-attachments", async (string id, ...) => { ... });
group.MapGet("/{id}/file-attachments", async (string id, ...) => { ... });
group.MapGet("/{id}/files/{attachmentId}/download", async (string id,
    string attachmentId, ...) =>
{
    var file = await fileStorage.GetFileAsync(attachmentId);
    return Results.File(file.Stream, file.ContentType, file.FileName);
});

Separate child endpoints are essential for file-heavy features. The frontend can load the Todo details, then lazily load attachments only when the user expands that section. This keeps initial page loads fast and lets the browser cache attachment data independently.

Request Size Limits for File Uploads

When a feature accepts file uploads, the endpoint needs a request size limit. The MVC Project Manager sets a 100 MB limit on create and update endpoints:

[RequestSizeLimit(104857600)] // 100 MB
group.MapPost("/", async (CreateTodoRequest request, IMediator mediator) =>
{
    var result = await mediator.Send(new CreateTodoCommand(request));
    return Results.Created($"/api/todo/{result.Data.Id}", result);
});

Without this, file uploads could exhaust server memory or be rejected by Kestrel's default 30 MB limit. The limit is set per-endpoint, not globally, so file-heavy features get higher limits while lightweight endpoints stay restricted.

Hybrid MVC + Minimal API: Two Entry Points, One Feature

The MVC Project Manager demonstrates a powerful pattern: MVC controllers for Razor views coexisting with Minimal APIs for REST. The TodoController handles page navigation (Index, Create, Edit, Detail) while TodoEndpoint handles API calls. Both live in the same feature folder:

Areas/Main/Todo/
├── Controllers/TodoController.cs   ← MVC routes (pages)
├── Endpoints/TodoEndpoint.cs       ← REST API routes
├── Cqrs/                           ← shared handlers
└── Views/                          ← Razor views

The controller and endpoints call the same CQRS handlers. This means the business logic is written once, and both the page-based MVC flow and the AJAX-based API flow use it identically. The VSA folder structure makes this natural — both entry points are just files in the feature folder.

Endpoint Registration in Program.cs

Register all feature endpoints with extension methods. Each feature gets one line:

var app = builder.Build();
app.MapTodoEndpoints();       // Todo feature
app.MapCurrencyEndpoints();   // Currency feature
app.MapTaxEndpoints();        // Tax feature

This keeps Program.cs clean and makes feature registration explicit. Removing a feature is as simple as commenting out one line. The extension method pattern is the standard way to organize VSA endpoints in .NET 10.

Key Takeaways

  • MapGroup creates a shared route prefix, auth policy, and OpenAPI tag for all endpoints in a feature
  • JWT authorization at the group level applies to every endpoint — no per-route [Authorize] needed
  • Separate child data endpoints (/items, /attachments) enable lazy loading and independent caching
  • RequestSizeLimit on file upload endpoints prevents memory exhaustion
  • Hybrid MVC + Minimal API lets Razor views and REST APIs share the same CQRS handlers in one feature folder

Frequently Asked Questions

Q: Should I use Minimal APIs or Controllers with VSA?

Use Minimal APIs for REST endpoints — they're more concise and work naturally with VSA's per-feature organization. Use Controllers only when you need Razor views (MVC pattern). The hybrid approach in the MVC Project Manager shows both coexisting in the same feature folder.

Q: How do I add authorization to all VSA endpoints at once?

Use MapGroup with RequireAuthorization. Every endpoint under the group inherits the auth policy. For JWT, add .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser(). For role-based, add .RequireRole("Admin", "Member").

Q: Can I mix MVC controllers and Minimal APIs in the same VSA feature?

Yes. The MVC Project Manager does exactly this — TodoController serves Razor pages, TodoEndpoint serves REST APIs, and both call the same CQRS handlers. The VSA folder structure doesn't care about the framework choice — only that everything for the feature is in one folder.

Q: What's the best way to handle file downloads in VSA endpoints?

Create a dedicated download endpoint like GET /{id}/files/{attachmentId}/download. Use Results.File(stream, contentType, fileName) to stream the file. The FileStorageService handles physical file retrieval. We'll cover the full file storage pattern in Part 7.

Part 5 of 20 in the VSA Todo App Tutorial Series | ← Previous: Part 4|Next: Part 6 →

Ready to study real VSA code?

Every Indotalent product is a complete .NET 10 application built with Vertical Slice Architecture. Complete source code — $21 each.

Explore Products

Looking for a ready-to-use traditional monolithic multilayered clean architecture?

Monolithic Clean Architecture — 1,300+ devs, 500+ forks. Clean Arch + CQRS + Repository Pattern. Free & open source for commercial use. ⭐ Star our repo or ❤️ buy our products — your support means everything!

Star on GitHub