DEV Community

Noof Saeed
Noof Saeed

Posted on

Building a Maintainable ASP.NET Core Minimal API

Minimal APIs in ASP.NET Core make it possible to build HTTP APIs with very little boilerplate.

But there is a common misunderstanding:

«Minimal API does not mean minimal architecture.»

When developers start using Minimal APIs, it is tempting to put everything inside Program.cs.

At first, this looks perfectly fine:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/api/company", async (CompanyDbContext db) =>
{
    var company = await db.Companies.FirstOrDefaultAsync();

    return company is null
        ? Results.NotFound()
        : Results.Ok(company);
});

app.Run();

Enter fullscreen mode Exit fullscreen mode

For a small API with a few endpoints, this can work. But as the application grows, Program.cs can quickly become bloated and difficult to maintain.


Minimal does not mean everything in one file

The goal of Minimal APIs is to reduce unnecessary ceremony around HTTP endpoints. It does not mean that every responsibility should live in the same place.

A better approach is to keep the endpoint routing simple while separating application responsibilities.

For example:

CompanyProfile
│
├── API
│   └── Endpoints
│       └── CompanyEndpoints.cs
│
├── Infrastructure
│   └── Data
│       └── CompanyDbContext.cs
│
├── Core
|     └── DTOs
|     └── Entities
│
└── Program.cs

Enter fullscreen mode Exit fullscreen mode

Now Program.cs can focus strictly on application configuration and startup.


Moving endpoints out of Program.cs (Separating Endpoints from Handlers)

Instead of writing inline lambdas inside the routing methods, we can separate the Endpoint Mapping from the Handler Logic.

This keeps our routes declarative and organized:

public static class CompanyEndpoints
{
    public static void MapCompanyEndpoints(this IEndpointRouteBuilder app)
    {
        var publicGroup = app.MapGroup("/api").WithTags("Public Website API");

         publicGroup.MapGet("/info", GetCompanyInfo)
            .WithSummary("Get Company Information");

    }

    // Explicit Handler Function
    private static async Task<IResult> GetCompanyInfo(AppDbContext db)
   {
    var info = await db.CompanyInformation.FirstOrDefaultAsync();
    return info is null
        ? Results.NotFound()
        : Results.Ok(new CompanyInfoResponseDto(info.Name, info.Description, info.Vision,            info.Mission));
   }

}

Enter fullscreen mode Exit fullscreen mode

Why separate the Handler?

  1. Clean Route Definitions: Route mappings read like a table of contents without noise.
  2. Better Testability: Handlers can be invoked or unit-tested directly without setting up full HTTP pipelines.
  3. Reusability & Readability: Keeps routing methods concise and clean.

Then register the endpoints cleanly from Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Add services...

var app = builder.Build();

app.MapCompanyEndpoints();

app.Run();

Enter fullscreen mode Exit fullscreen mode

Why separate endpoints?

As the number of endpoints increases, organization becomes critical. Instead of having a single monolithic file, group related endpoints together:

Endpoints
├── CompanyEndpoints.cs
├── ExampleEndpoints.cs
└── ExampleEndpoints2.cs

Enter fullscreen mode Exit fullscreen mode

Now each endpoint module has a single responsibility.


Minimal APIs and Dependency Injection

Minimal APIs work naturally with ASP.NET Core's Dependency Injection system. Dependencies can be injected directly into handler parameters:


private static async Task<IResult> GetServices(AppDbContext db)
{
    var services = await db.Services
        .Select(s => new ServiceResponseDto(s.Id, s.Title, s.Description, s.Icon))
        .ToListAsync();
    return Results.Ok(services);
}

Enter fullscreen mode Exit fullscreen mode

There is no need to manually instantiate services—ASP.NET Core resolves them via DI per request scope.


Minimal APIs with Entity Framework Core

When working with EF Core inside handlers, always aim for efficient queries by leveraging features like AsNoTracking() for read-only endpoints:

private static async Task<IResult> GetServices(AppDbContext db)
{
    var services = await db.Services
        .AsNoTracking()
        .Select(s => new ServiceResponseDto(s.Id, s.Title, s.Description, s.Icon))
        .ToListAsync();
    return Results.Ok(services);
}

Enter fullscreen mode Exit fullscreen mode

For larger domain logic, you can seamlessly delegate tasks to dedicated service layers or repositories when application complexity demands it.


What about Controllers?

Minimal APIs are not necessarily a replacement for Controllers. Both approaches have their place.

Minimal APIs excel when you want:

  • Reduced boilerplate
  • Lightweight HTTP endpoints
  • Microservices & lightweight background modules
  • Performance-focused, endpoint-centric routing

Controllers remain valuable when:

  • Working with legacy MVC applications
  • Relying heavily on controller-level action filters and complex conventions

The question isn't "Which one is better?", but rather "Which approach fits the system I am building?"


The Important Distinction

Minimal API
    ↓
Minimal endpoint ceremony

Minimal architecture
    ↓
Everything squeezed into Program.cs

Enter fullscreen mode Exit fullscreen mode

The first is a modern design feature. The second is an architectural anti-pattern.


A Practical Example

I applied this structure while building a clean Company Profile API using:

  • ASP.NET Core Minimal APIs
  • Entity Framework Core
  • Dependency Injection
  • OpenAPI / Scalar UI
  • Modular Endpoint Organization

🔗 GitHub Repository: NoofSaeed/CompanyProfile


Final Thoughts

Minimal APIs are one of the most powerful additions to modern .NET. Their simplicity is valuable, but simplicity should not be confused with lack of structure.

A maintainable Minimal API relies on:

  • Clean, decoupled Handlers
  • Modular Endpoint Registration
  • Dependency Injection
  • Well-structured Project Layout

The syntax stays minimal while the architecture remains scalable.

Top comments (0)