Learnixo
Back to blog
AI Systemsintermediate

Route Groups — Organizing Minimal APIs at Scale

Use MapGroup to structure Minimal API endpoints: shared prefixes, shared middleware, auth policies per group, nested groups, and the endpoint organization patterns that replace controllers.

Asma Hafeez KhanMay 16, 20264 min read
Minimal APIsRoute GroupsASP.NET Core.NETOrganization
Share:𝕏

Why Route Groups

Without groups, Minimal APIs become a flat list of app.Map*() calls in Program.cs. Route groups add hierarchy: shared path prefix, shared middleware, shared authorization — all applied to a set of endpoints.

C#
// Without groups — flat, repetitive
app.MapGet   ("/api/v1/patients",      GetAll).RequireAuthorization();
app.MapPost  ("/api/v1/patients",      Create).RequireAuthorization();
app.MapGet   ("/api/v1/patients/{id}", GetById).RequireAuthorization();
app.MapPut   ("/api/v1/patients/{id}", Update).RequireAuthorization();
app.MapDelete("/api/v1/patients/{id}", Delete).RequireAuthorization();

// With groups — DRY
var patients = app.MapGroup("/api/v1/patients").RequireAuthorization();
patients.MapGet   ("",     GetAll);
patients.MapPost  ("",     Create);
patients.MapGet   ("/{id}", GetById);
patients.MapPut   ("/{id}", Update);
patients.MapDelete("/{id}", Delete);

Basic Route Group

C#
// Group with shared prefix
var api = app.MapGroup("/api/v1");

// Sub-groups within api
var patients     = api.MapGroup("/patients");
var prescriptions = api.MapGroup("/prescriptions");
var pharmacy     = api.MapGroup("/pharmacy");

// Endpoints within each group
patients.MapGet("",           GetAllPatients);
patients.MapPost("",          CreatePatient);
patients.MapGet("/{id:guid}", GetPatient);
patients.MapPut("/{id:guid}", UpdatePatient);

prescriptions.MapGet("",           GetAllPrescriptions);
prescriptions.MapPost("",          CreatePrescription);
prescriptions.MapGet("/{id:guid}", GetPrescription);

Groups with Authorization

C#
// Auth applied at group level — all endpoints inherit
var publicRoutes = app.MapGroup("/api/public")
    .AllowAnonymous();

var authRoutes = app.MapGroup("/api")
    .RequireAuthorization();  // any authenticated user

var doctorRoutes = app.MapGroup("/api/clinical")
    .RequireAuthorization("DoctorsOnly");

var pharmacyRoutes = app.MapGroup("/api/pharmacy")
    .RequireAuthorization("PharmacyStaff");

var adminRoutes = app.MapGroup("/api/admin")
    .RequireAuthorization("AdminOnly");

// Public endpoints
publicRoutes.MapGet("/health", () => Results.Ok("healthy"));
publicRoutes.MapPost("/auth/login", Login);

// Doctor-only clinical endpoints
doctorRoutes.MapGet("/patients",         GetPatients);
doctorRoutes.MapPost("/prescriptions",   CreatePrescription);
doctorRoutes.MapGet("/drug-interactions",CheckInteractions);

// Override group policy for a specific endpoint
doctorRoutes.MapGet("/patients/{id}", GetPatient)
    .RequireAuthorization("DoctorsOrNurses");  // overrides "DoctorsOnly"

Organizing Endpoints Into Extension Methods

C#
// Split endpoint registration across files — one file per resource
// Api/Endpoints/PatientEndpoints.cs
public static class PatientEndpoints
{
    public static RouteGroupBuilder MapPatients(this RouteGroupBuilder group)
    {
        group.MapGet("",            GetAll);
        group.MapPost("",           Create);
        group.MapGet("/{id:guid}",  GetById);
        group.MapPut("/{id:guid}",  Update);
        group.MapDelete("/{id:guid}",Delete);
        return group;
    }

    private static async Task<IResult> GetAll(
        AppDbContext db, CancellationToken ct) { /* ... */ }

    private static async Task<IResult> Create(
        CreatePatientDto dto, CreatePatientHandler handler, CancellationToken ct)
    { /* ... */ }

    // ... other handlers
}

// Api/Endpoints/PrescriptionEndpoints.cs
public static class PrescriptionEndpoints
{
    public static RouteGroupBuilder MapPrescriptions(
        this RouteGroupBuilder group)
    {
        group.MapGet("",            GetAll);
        group.MapPost("",           Create);
        group.MapGet("/{id:guid}",  GetById);
        return group;
    }
    // ...
}

// Program.cs — clean registration
var doctorRoutes = app.MapGroup("/api/clinical").RequireAuthorization("DoctorsOnly");
doctorRoutes.MapGroup("/patients").MapPatients();
doctorRoutes.MapGroup("/prescriptions").MapPrescriptions();

Nested Groups

C#
// Patient → Prescriptions nested resource
var api      = app.MapGroup("/api/v1");
var patients = api.MapGroup("/patients").RequireAuthorization();

// Nested group: /api/v1/patients/{patientId}/prescriptions
var patientRx = patients.MapGroup("/{patientId:guid}/prescriptions");

patientRx.MapGet("",           GetPatientPrescriptions);
patientRx.MapPost("",          AddPrescriptionToPatient);
patientRx.MapGet("/{rxId:guid}", GetSpecificPrescription);
patientRx.MapDelete("/{rxId:guid}", DiscontinuePrescription);

Shared Metadata on Groups

C#
// Apply OpenAPI tags to all endpoints in the group
var patientsGroup = app.MapGroup("/api/patients")
    .WithTags("Patients")           // Swagger/Scalar UI grouping
    .WithOpenApi()
    .RequireAuthorization()
    .AddEndpointFilter<AuditLogFilter>()  // log all patient endpoint access
    .WithMetadata(new CacheControlMetadata { MaxAge = 0 });  // no cache for patient data

Versioning with Groups

C#
// Route-based versioning
var v1 = app.MapGroup("/api/v1");
var v2 = app.MapGroup("/api/v2");

// V1 endpoints
v1.MapGroup("/patients").MapPatients_V1();

// V2 endpoints — new behavior, new DTOs
v2.MapGroup("/patients").MapPatients_V2();

// Header-based versioning (requires ApiVersioning package)
// builder.Services.AddApiVersioning();
// builder.Services.AddVersionedApiExplorer();

Groups vs Controllers

Controllers:
  ✓ Familiar pattern for teams coming from MVC
  ✓ Built-in [Authorize], [HttpGet], action filters
  ✗ Heavier: class instantiation, action invoker pipeline

Route Groups (MapGroup):
  ✓ Lightweight — delegates, no class overhead
  ✓ Auth, middleware, filters at group level
  ✓ Composable, testable
  ✓ Full performance of Minimal APIs
  ✗ Less familiar to teams new to Minimal APIs

Recommendation: MapGroup for new projects or performance-sensitive APIs.
                Controllers for teams transitioning from MVC.

Red Flag / Green Answer

Red Flag: "We have 200 app.MapGet/Post/Put/Delete() calls directly in Program.cs — it's 800 lines long."

Large flat Program.cs is unmaintainable. No shared auth, no shared middleware, every endpoint is a separate declaration. Add a change and find the right endpoint among 200 entries.

Green Answer:

MapGroup per resource, auth at group level. Endpoint registration split into static extension methods per file. Program.cs is under 50 lines of wiring code.


Key Takeaway

MapGroup brings organization to Minimal APIs: shared path prefix, shared authorization, shared filters and metadata. Nest groups for resource hierarchies. Split endpoint declarations into RouteGroupBuilder extension methods per resource — one file per domain concept. Program.cs wires groups together; endpoint files contain the implementation. This scales to hundreds of endpoints without Program.cs becoming unmanageable.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.