Skip to content

.NET Minimal APIs:

Apparently We’ve Finally Admitted Controllers Were a Mistake

An entirely unbiased account from a developer who has been writing front ends, back ends, and increasingly desperate resignation letters for about 30 years.

I have been a software developer for roughly 30 years.

That means I have witnessed the entire evolution of web development, from:

“Let’s put some HTML on a server.”

to:

“Let’s build a distributed, event-driven, cloud-native, containerized, observable, horizontally scalable platform that renders a button.”

I have used technologies whose names have since been erased from the archaeological record.

I remember when we thought frames were a good idea.

I remember when JavaScript was something you apologized for putting on a page.

I remember when XML was going to solve everything.

I remember when SOAP was going to solve everything.

I remember when REST was going to solve everything.

I have learned one important thing from all of this:

We are really bad at leaving well enough alone.

And then along came .NET Minimal APIs.

And I thought:

“Wait a minute.

Are we… just writing functions now?”

Apparently.


The Good Old Days

When I started building web applications, the web stack was delightfully primitive.

You had a server.

You had a database.

You had HTML.

You had some JavaScript, although admitting this publicly could get you fired.

And you had a vague understanding that HTTP involved something called GET and POST.

Then frameworks arrived.

And frameworks did what frameworks do best:

They created an enormous amount of infrastructure to protect you from having to write three lines of code.

Before long, creating an endpoint required approximately seventeen files.

You had:

  • A controller
  • An interface
  • A service
  • A service implementation
  • A repository
  • A repository interface
  • A DTO
  • A DTO mapper
  • A validator
  • A dependency injection registration
  • An extension method to register the dependency injection registration
  • A configuration class
  • An options class
  • An AutoMapper profile
  • A unit test
  • A mock of the repository
  • A mock of the service
  • A mock of the mapper
  • And, if you were really serious, a factory for the mock factory

Eventually someone would ask:

“Can you add an endpoint that returns a customer by ID?”

And you’d say:

“Sure. Give me two days.”


Enter Minimal APIs

Then Microsoft basically looked at all of this and said:

“What if you just… didn’t?”

And thus Minimal APIs were born.

You can now write something like:

app.MapGet("/customers/{id}", async (int id, MyDbContext db) =>
{
    return await db.Customers.FindAsync(id);
});

That’s it.

That’s the endpoint.

There isn’t a controller.

There isn’t a controller factory.

There isn’t a controller interface.

There isn’t a controller base class.

There isn’t a controller factory factory.

There is just:

GET /customers/42

and some C#.

At first, I didn’t trust it.

I’ve been doing this too long.

Whenever something looks simple, I immediately assume Microsoft has hidden something horrible behind it.


“But Where’s the Controller?”

This is the first question every veteran developer asks.

You see:

app.MapGet("/orders/{id}", (int id) =>
{
    return GetOrder(id);
});

And your brain immediately starts looking around the room.

“Where’s the controller?”

Nothing.

“Where’s the action method?”

That’s it.

“Where’s the attribute?”

There isn’t one.

“Where’s the base class?”

There isn’t one.

“Where do I put [Authorize]?”

Right there.

“Where do I put [ProducesResponseType]?”

Right there.

“Where’s the 900-line startup configuration file?”

Gone.

At this point you become suspicious.

You start walking around the codebase looking behind things.


The Front-End Developer’s Perspective

I’ve spent decades doing both front-end and back-end development, which means I have developed the unique ability to be disappointed by both sides of the application.

The front end says:

“The API is returning the wrong shape.”

The back end says:

“The front end is consuming the API incorrectly.”

The database says:

“Both of you are idiots.”

Minimal APIs actually make this relationship slightly more honest.

Your endpoint is sitting there in plain sight:

app.MapPost("/products", async (Product product, MyDbContext db) =>
{
    db.Products.Add(product);
    await db.SaveChangesAsync();

    return Results.Created($"/products/{product.Id}", product);
});

The front-end developer can look at it and understand what is happening.

The back-end developer can look at it and understand what is happening.

The database developer can look at it and say:

“Why are you returning the entity?”

And thus, balance is restored to the universe.


The Beautiful Thing About Minimal APIs

Minimal APIs acknowledge something we’ve collectively refused to admit for decades:

Sometimes an endpoint is just an endpoint.

Sometimes you don’t need an architectural ceremony around it.

Sometimes:

HTTP request
   ↓
do thing
   ↓
HTTP response

is actually a perfectly respectable architecture.

This is especially useful for small services.

Health checks.

Webhooks.

Internal APIs.

Tiny microservices.

Background-service control endpoints.

Prototypes.

Administrative APIs.

That one endpoint you needed because somebody in accounting wants to trigger a nightly report by hitting a URL.

For those things, Minimal APIs are fantastic.

You don’t need to construct the Cathedral of Notre Dame because someone asked for a GET endpoint.


But Then We Discover Dependency Injection

Here’s where things get interesting.

Minimal APIs support dependency injection directly in handler parameters.

So this:

app.MapGet("/customers/{id}",
    async (int id, CustomerService service) =>
        await service.GetCustomer(id));

is perfectly reasonable.

And suddenly I’m thinking:

“Wait. This is actually pretty nice.”

The framework sees CustomerService and says:

“Oh, you need one of those? Here.”

The handler gets its dependencies.

Nobody has to manually dig around inside some mystical HttpContext.Services object like they’re searching for a lost contact lens.

You can even inject multiple dependencies:

app.MapGet("/reports",
    async (
        ReportService reports,
        ILogger<Program> logger,
        IConfiguration config,
        CancellationToken cancellationToken) =>
    {
        logger.LogInformation("Generating report");

        return await reports.GenerateAsync(cancellationToken);
    });

At this point, the method signature starts looking like the guest list at a family reunion.

But at least it’s honest.


Filters: Because We Can’t Have Nice Things

Eventually someone says:

“This is great, but we need cross-cutting concerns.”

And the veteran developer’s eye twitches.

Because we know what happens next.

Someone needs:

  • authorization
  • validation
  • logging
  • exception handling
  • telemetry
  • rate limiting
  • transactions
  • auditing
  • correlation IDs
  • request timing
  • feature flags
  • some weird business rule from 2017

And suddenly our beautiful little Minimal API endpoint has become:

app.MapPost("/widgets",
    async (
        WidgetRequest request,
        WidgetService service,
        Validator validator,
        AuditService audit,
        ILogger<Program> logger,
        CancellationToken cancellationToken) =>
    {
        ...
    })
    .RequireAuthorization()
    .AddEndpointFilter<ValidationFilter>()
    .AddEndpointFilter<AuditFilter>();

Congratulations.

We have reinvented enterprise architecture.

It took approximately 11 minutes.


The Endpoint Group

Minimal APIs do have a nice answer to this.

Groups.

You can do something like:

var admin = app.MapGroup("/admin")
    .RequireAuthorization("AdminPolicy");

admin.MapGet("/users", ...);
admin.MapGet("/reports", ...);
admin.MapDelete("/users/{id}", ...);

This is one of those features where you stare at it for a moment and think:

“That’s actually sensible.”

Which is always dangerous.

Because the next meeting will inevitably contain the phrase:

“Let’s create a generic endpoint group abstraction.”

And now you’re back to square one.


OpenAPI: The Thing We Used to Call Swagger

One of the things I genuinely appreciate about modern .NET is how much API metadata can be generated without turning the application into a ritual sacrifice.

Minimal APIs integrate nicely with OpenAPI tooling.

You can define an endpoint:

app.MapGet("/customers/{id}",
    async (int id, CustomerService service) =>
        await service.Get(id));

and expose useful API documentation.

This is important because apparently we have reached the point in civilization where humans can build distributed systems spanning twelve countries but still need a webpage to tell them whether an endpoint expects:

{
    "customerId": 123
}

or:

{
    "id": 123
}

or:

{
    "customer_id": 123
}

or, my personal favorite:

{
    "customer": {
        "customer": {
            "customerId": 123
        }
    }
}

OpenAPI exists because apparently naming things was the second-hardest problem in computer science.


The Problem: You Can Make a Horrible Minimal API

This is the part where I have to issue a warning.

Minimal APIs make it extremely easy to write bad code.

You can absolutely do this:

app.MapPost("/orders", async (
    Order order,
    SqlConnection connection,
    ILogger<Program> logger) =>
{
    // 900 lines of business logic
});

Please don’t.

That’s not a Minimal API.

That’s a cry for help.

The fact that you can put everything into Program.cs does not mean you should.

We’ve learned this lesson before.

In the 1990s we put everything into one HTML page.

Then we put everything into one JavaScript file.

Then we put everything into one controller.

Then we put everything into one service.

Now we are being offered Program.cs.

Do not make the same mistake again.


Minimal Does Not Mean Architectural

This distinction is important.

Minimal APIs are about reducing the ceremony around HTTP endpoints.

They don’t magically eliminate:

  • business logic
  • domain modeling
  • persistence concerns
  • security
  • testing
  • observability
  • validation
  • maintainability

You still need architecture.

You just don’t necessarily need architecture cosplay.

There’s a difference between:

“We have a clean separation of concerns.”

and:

“We have 46 projects so nobody can figure out where the code lives.”

I’ve worked on both.

I know which one I prefer.


Thirty Years of Front-End Development Has Taught Me One Thing

The back end and front end have more in common than we like to admit.

Front-end developers spent years trying to make HTML simpler.

Then we invented:

  • MVC
  • MVVM
  • component frameworks
  • state management
  • stores
  • hooks
  • signals
  • observables
  • reactive programming
  • meta-frameworks
  • server components
  • build pipelines

Eventually someone says:

“Here’s a button.”

And the answer is:

“Great. Which state container owns it?”

The back end did exactly the same thing.

We started with:

GET /customers/42

and eventually arrived at:

Controller
 → Mediator
 → Command
 → Handler
 → Domain Service
 → Repository
 → Unit of Work
 → ORM
 → Database

Minimal APIs are, in a strange way, a reminder that we can occasionally just write:

app.MapGet("/customers/{id}", ...);

and get on with our lives.


The Most Dangerous Sentence in Software

After 30 years, I have learned that the most dangerous sentence in software development is:

“Wouldn’t it be cool if…?”

Wouldn’t it be cool if our API were event-driven?

Wouldn’t it be cool if everything were microservices?

Wouldn’t it be cool if we used GraphQL?

Wouldn’t it be cool if we had CQRS?

Wouldn’t it be cool if we put Kubernetes in front of it?

Wouldn’t it be cool if we created our own abstraction over Microsoft’s abstraction?

And now:

“Wouldn’t it be cool if we made a Minimal API?”

Yes.

It would.

Just remember that minimal code isn’t the same thing as minimal thinking.


So, Are Minimal APIs Good?

Here’s my very sophisticated 30-year technical assessment:

It depends.

Yes, I know.

That’s disappointing.

You were hoping for a definitive answer.

Sorry.

I spent three decades learning that definitive answers in software are usually just marketing copy with better posture.

Minimal APIs are excellent when you want:

  • small APIs
  • focused services
  • straightforward endpoints
  • lightweight HTTP services
  • webhooks
  • internal tools
  • prototypes
  • simple CRUD APIs

They’re less compelling when you have a massive application with hundreds of complicated endpoints and a team that strongly benefits from conventional controller organization and conventions.

And that’s okay.

You don’t get extra developer points for using the newest thing.

Nobody gets to Heaven because they successfully replaced 47 controllers with lambda expressions.


The Real Innovation

The most interesting thing about Minimal APIs isn’t actually that they’re minimal.

It’s that modern .NET finally seems comfortable admitting:

Not every application needs a framework-shaped building around it.

Sometimes you need a skyscraper.

Sometimes you need a house.

Sometimes you need a garden shed with an HTTP endpoint.

And sometimes the requirement really is:

“When Stripe calls this URL, update the order and return 200.”

For that, maybe we don’t need twelve interfaces.

Maybe we don’t need a factory.

Maybe we don’t need a repository abstraction over Entity Framework which is itself abstracting a database abstraction.

Maybe we can just write the damn endpoint.


After 30 Years…

I’ve watched programming languages come and go.

I’ve watched architectures rise and fall.

I’ve watched JavaScript become a serious programming language.

I’ve watched CSS become something people write entire books about.

I’ve watched XML die, come back as configuration, and then die again.

I’ve watched the industry rediscover functional programming approximately every seven years.

And I’ve watched us repeatedly build enormous systems to solve problems that could have been solved with a function and a database.

So when I see this:

app.MapGet("/hello", () => "Hello, world!");

I don’t laugh.

I don’t complain.

I don’t ask where the controller is.

I just stare at it quietly.

Because somewhere deep inside my 30-year-old developer soul, beneath the scars from Web Forms and the psychological damage caused by Internet Explorer 6…

I think I’ve finally found peace.

Then someone walks into my office and says:

“We need to put this behind a service mesh.”

And just like that, I’m looking at jobs on LinkedIn again.

If you want, I can also turn this into a much more savage “old developer yelling at the cloud” version, with jokes about Web Forms, SOAP, Entity Framework, Angular, React, Docker, Kubernetes, and microservices.

Leave a Reply

Your email address will not be published. Required fields are marked *