·.NET·2 min read·Mid-level developers

Structuring Minimal APIs Without the Program.cs Monster

Minimal APIs are wonderful for the first 200 lines. Then Program.cs starts looking like a fanfic. Here's the pattern that keeps it sane.

The thing nobody warns you about with Minimal APIs is how quickly Program.cs mutates from "elegant" to "this is a 1,400-line war crime." The fix isn't to go back to Controllers. It's to apply the same separation Controllers already gave you, by hand.

The pattern I've landed on after three projects: one file per feature, an interface, and MapGroup doing the grunt work.

public interface IEndpoint
{
    void Map(IEndpointRouteBuilder app);
}

public class UsersEndpoints : IEndpoint
{
    public void Map(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/users").WithTags("Users");
        group.MapGet("/{id}", GetById);
        group.MapPost("/", Create).RequireAuthorization("admin");
    }

    static async Task<IResult> GetById(int id, IUserService svc) =>
        await svc.FindAsync(id) is { } u ? Results.Ok(u) : Results.NotFound();

    static async Task<IResult> Create(CreateUser req, IUserService svc) =>
        Results.Created($"/users/{await svc.AddAsync(req)}", null);
}

Registration is one extension method, called once from Program.cs:

public static IEndpointRouteBuilder MapEndpoints(this IEndpointRouteBuilder app)
{
    foreach (var t in typeof(IEndpoint).Assembly.GetTypes()
                 .Where(t => t is { IsInterface: false, IsAbstract: false }
                          && typeof(IEndpoint).IsAssignableFrom(t)))
        ((IEndpoint)Activator.CreateInstance(t)!).Map(app);
    return app;
}

Program.cs shrinks to 30 lines. Each feature owns its routes, its handlers, and its filters. New endpoint? New file. PR diff is tight. Onboarding is "go look at UsersEndpoints.cs."

A few things I'd warn against:

Don't try to be too clever with source generators here. The reflection version runs once at startup, takes microseconds, and doesn't break debugging. Save source generators for the hot path.

Don't put feature folders inside an Endpoints/ folder. Put them at the root next to your services and models. The folder structure should reflect features, not technical concerns. You'll appreciate it the third time you grep across "users".

And don't bring back Controllers just because Program.cs got big. The fix above is fifty lines and it stays fifty lines. Controllers come with a tax you only really notice once you've left them behind.