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.
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.
// 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
// 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
// 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
// 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
// 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
// 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 dataVersioning with Groups
// 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.csis 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:
MapGroupper resource, auth at group level. Endpoint registration split into static extension methods per file.Program.csis under 50 lines of wiring code.
Key Takeaway
MapGroupbrings organization to Minimal APIs: shared path prefix, shared authorization, shared filters and metadata. Nest groups for resource hierarchies. Split endpoint declarations intoRouteGroupBuilderextension methods per resource — one file per domain concept.Program.cswires groups together; endpoint files contain the implementation. This scales to hundreds of endpoints withoutProgram.csbecoming unmanageable.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.