Skip to main content
SEMastery
ASP.NETintermediate

Productive Web API Development with FastEndpoints and Vertical Slice Architecture in .NET

Learn how FastEndpoints and Vertical Slice Architecture work together to build clean, fast, and easy-to-grow web APIs in .NET 10, step by step.

14 min readUpdated January 16, 2026

A tailor shop where each suit is made in one station

Imagine two tailor shops near your home.

In the first shop, the work is split by job. One room only cuts cloth. Another room only stitches. A third room only adds buttons. To make one suit, your cloth travels from room to room, and three different people touch it. If a single customer wants a small change, the owner has to walk into every room to explain it. When the shop gets busy, things get lost between rooms.

In the second shop, each suit is made at one station by one team. The cutting, the stitching, and the buttons for that suit all happen in one place. If a customer wants a change, you go to that one station and fix it there. Adding a new style of suit is easy: you set up a new station for it. Nothing else gets disturbed.

Most older .NET apps are built like the first shop. They split code by type of work: a folder for controllers, a folder for services, a folder for repositories. One feature is scattered across all of them.

Vertical Slice Architecture (VSA) is the second shop. You split code by feature. Everything one feature needs sits together in one slice. And FastEndpoints is the perfect tool for this style, because it already wants you to write each endpoint as one small, self-contained class.

In this post we will see why these two work so well together, and build a small slice of a real API from scratch.

The problem with layers

Let us first see why the layered style starts to hurt as an app grows.

Say you have an online shop. To add a simple "create product" feature in the layered style, you usually touch:

  • a controller, in the Controllers folder
  • a service interface and a service class, in the Services folder
  • a repository, in the Repositories folder
  • a DTO or two, in the Models folder
  • a validator, somewhere else again

So one tiny feature is spread across five or six folders. To read the feature, you jump between files. To change it, you risk breaking other features that share the same service. New team members get lost. This is the "cloth travels from room to room" problem.

In layered architecture, one feature is spread across many technical folders.

The layers are not evil. For a very small or very stable app they can be fine. But as features pile up, the cost of jumping between folders grows, and shared services become tangled knots that are scary to change.

What Vertical Slice Architecture changes

Vertical Slice Architecture flips the folder structure. Instead of grouping by layer, you group by feature. Each feature is a slice that cuts straight down through all the layers it needs.

A slice for "create product" holds its own request, its own validation, its own handling logic, and its own response, all in one folder (often one file). The slice next to it, "get product", is completely separate. They do not share a giant service class, so changing one cannot accidentally break the other.

In Vertical Slice Architecture, each feature is one self-contained slice.

Here is the same idea written as a simple comparison.

QuestionLayered ArchitectureVertical Slice Architecture
How is code grouped?By technical layerBy feature
Where does one feature live?Spread across many foldersTogether in one slice
To add a feature you...Edit many shared filesAdd a new slice
Risk of breaking other featuresHigher (shared services)Lower (isolated slices)
Easy for new people to read?Often noUsually yes
Best forSmall or stable appsApps that keep growing

Why FastEndpoints is a natural fit

FastEndpoints is a free, open-source framework for building REST APIs in ASP.NET Core 8 and newer. It runs on top of normal ASP.NET Core, so the routing, middleware, and dependency injection are the same ones you already know.

Its core idea is the REPR pattern, which stands for Request, Endpoint, Response. (Say it out loud and it sounds like "reaper".) Each endpoint becomes its own class:

  • Request: a small class describing what the client sends.
  • Endpoint: a class holding the logic for exactly one route.
  • Response: a small class describing what you send back.

Do you see it? That single endpoint class is already a vertical slice. The request, the response, the validation, and the logic all sit together. FastEndpoints does not just allow VSA; it gently pushes you straight into it. This is why the .NET community keeps pairing the two together.

How a request flows through one slice

Client sends request
Bind to Request DTO
Validate input
Run handler logic
Return Response DTO

Steps

1

Client sends request

JSON over HTTP

2

Bind to Request DTO

fill the request object

3

Validate input

rules run automatically

4

Run handler logic

HandleAsync does the work

5

Return Response DTO

send JSON back

One request enters one endpoint class and one response comes back out.

Setting up the project

Let us build a small slice of a Products API. First, create a new empty web project and add the package.

dotnet new web -n ShopApi
cd ShopApi
dotnet add package FastEndpoints

Now open Program.cs and wire FastEndpoints in. It needs two lines: one to register its services, and one to add it to the request pipeline.

using FastEndpoints;
 
var builder = WebApplication.CreateBuilder(args);
 
// Register FastEndpoints services
builder.Services.AddFastEndpoints();
 
var app = builder.Build();
 
// Add FastEndpoints to the request pipeline
app.UseFastEndpoints();
 
app.Run();

That is the whole setup. FastEndpoints will scan your project for endpoint classes and register each one automatically. You never write a big list of routes by hand.

Folder layout for slices

Before we write code, let us decide where files go. In VSA we group by feature, so we make a Features folder, and inside it a folder per feature area. Each endpoint slice gets its own file.

A feature-first folder layout

Features
Products
CreateProduct.cs
GetProduct.cs

Steps

1

Features

top-level folder

2

Products

one feature area

3

CreateProduct.cs

one full slice

4

GetProduct.cs

another full slice

Group folders by feature, not by technical layer.

A typical tree looks like this:

ShopApi/
  Program.cs
  Features/
    Products/
      CreateProduct.cs
      GetProduct.cs
      Product.cs        # the shared domain model

Notice there is no Controllers folder, no Services folder, and no Repositories folder. Each slice file holds everything that slice needs.

Writing the first slice: create a product

Here is a complete "create product" slice in one file. It has the request, the response, the validation rules, and the logic, all together. This is the heart of how FastEndpoints and VSA meet.

using FastEndpoints;
using FluentValidation;
 
namespace ShopApi.Features.Products;
 
// 1. Request: what the client sends
public sealed class CreateProductRequest
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
}
 
// 2. Response: what we send back
public sealed class CreateProductResponse
{
    public Guid Id { get; set; }
    public string Name { get; set; } = string.Empty;
}
 
// 3. Validation: rules for the request
public sealed class CreateProductValidator : Validator<CreateProductRequest>
{
    public CreateProductValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Please give the product a name.")
            .MaximumLength(100);
 
        RuleFor(x => x.Price)
            .GreaterThan(0).WithMessage("Price must be more than zero.");
    }
}
 
// 4. Endpoint: the slice's logic
public sealed class CreateProductEndpoint
    : Endpoint<CreateProductRequest, CreateProductResponse>
{
    public override void Configure()
    {
        Post("/products");
        AllowAnonymous();
    }
 
    public override async Task HandleAsync(CreateProductRequest req, CancellationToken ct)
    {
        // In a real app you would save to a database here.
        var product = new CreateProductResponse
        {
            Id = Guid.NewGuid(),
            Name = req.Name
        };
 
        await SendAsync(product, statusCode: 201, cancellation: ct);
    }
}

Read that file top to bottom. You can understand the entire feature without opening any other file. The validation runs automatically before HandleAsync, so by the time your logic runs, the input is already clean. If the validation fails, FastEndpoints sends back a clear 400 response with the messages, and your handler never runs.

Writing the second slice: get a product

A second slice lives in its own file and does not touch the first one. Here the route has a parameter, so we read GET /{id} from the URL into the request DTO.

using FastEndpoints;
 
namespace ShopApi.Features.Products;
 
public sealed class GetProductRequest
{
    public Guid Id { get; set; }
}
 
public sealed class GetProductEndpoint
    : Endpoint<GetProductRequest, CreateProductResponse>
{
    public override void Configure()
    {
        Get("/products/{id}");
        AllowAnonymous();
    }
 
    public override async Task HandleAsync(GetProductRequest req, CancellationToken ct)
    {
        // Pretend we looked this up in a database.
        if (req.Id == Guid.Empty)
        {
            await SendNotFoundAsync(ct);
            return;
        }
 
        var product = new CreateProductResponse
        {
            Id = req.Id,
            Name = "Sample product"
        };
 
        await SendOkAsync(product, ct);
    }
}

The id in the route GET /{id} is matched to the Id property of the request automatically. This is part of the built-in model binding that FastEndpoints gives you, and it is more flexible than the model binding in plain Minimal APIs.

Now, to add a third feature like "delete product", you add a new file. You do not edit CreateProduct.cs or GetProduct.cs at all. That is the whole promise of slices: change is local.

Keeping slices decoupled without MediatR

Sometimes one slice needs to tell other parts of the app that something happened, for example "a product was created". For years, .NET tutorials reached for MediatR to do this. But there is important news: MediatR moved to a paid commercial license in 2025, and so did MassTransit. You do not have to pay for this pattern.

FastEndpoints has its own lightweight command bus and event bus built right in, at no cost. You can publish an event from one slice and let other handlers react, without any tight coupling and without a paid library.

using FastEndpoints;
 
namespace ShopApi.Features.Products;
 
// An event: "a product was created"
public sealed class ProductCreated : IEvent
{
    public Guid Id { get; set; }
    public string Name { get; set; } = string.Empty;
}
 
// A handler that reacts to that event
public sealed class SendWelcomeEmail : IEventHandler<ProductCreated>
{
    public Task HandleAsync(ProductCreated e, CancellationToken ct)
    {
        // e.g. log it, send a notification, update a cache...
        Console.WriteLine($"Product created: {e.Name}");
        return Task.CompletedTask;
    }
}

Inside an endpoint you publish the event with one line:

await PublishAsync(new ProductCreated { Id = product.Id, Name = req.Name }, cancellation: ct);

The endpoint does not know or care who is listening. New reactions can be added as new handlers in their own files. The slices stay loosely coupled, which is exactly what we want.

One slice publishes an event and other handlers react, without tight coupling.

How it compares to other styles

You can build VSA with controllers or Minimal APIs too. But FastEndpoints makes it the path of least resistance. Here is an honest comparison based on recent .NET 10 use.

PointControllers (MVC)Minimal APIsFastEndpoints
Natural fit for VSAWeak (fat controllers)Okay (needs discipline)Strong (one class per slice)
Boilerplate per endpointHighLowLow
Built-in validationManual wiringManual wiringBuilt in (FluentValidation)
Built-in command/event busNoNoYes (free)
Performance vs controllersBaselineFaster, lighterFaster, lighter
Auto endpoint discoveryNoNoYes

None of these are wrong. If you have a huge legacy MVC app, stay on controllers. If you want the tiniest possible serverless function, Minimal APIs are great. But if you want clean feature slices with very little ceremony, FastEndpoints is hard to beat.

A simple rule for choosing your approach

When you are not sure how to start a new API, this small decision flow helps.

A quick way to decide between layered and vertical slice styles.

Tips for healthy slices

As your app grows, a few habits keep your slices clean and friendly.

  • One slice, one job. If a slice starts doing two unrelated things, split it into two.
  • Share only true domain code. It is fine for slices to share a Product model or a database context. Avoid sharing a giant "ProductService" that every slice depends on, because that brings back the tangled knot.
  • Put validation in the slice. Keep the validator in the same file as its request. The rules belong to that feature.
  • Name files by feature. CreateProduct.cs, not ProductController.cs. The name should tell you what the slice does.
  • Use the built-in bus, not a paid library. Reach for FastEndpoints events before adding MediatR or MassTransit, since those now cost money.
  • Group folders by feature area. A Products folder, an Orders folder, and so on. This scales nicely to hundreds of features.

These small rules keep the "one station per suit" feeling even when the shop gets very busy.

A quick word on testing

Because each slice is small and self-contained, it is also easy to test. You can test the validator on its own with plain inputs, and you can test the endpoint logic without spinning up the whole app. FastEndpoints also ships helpers for integration tests that send a real request to your endpoint and check the response. Small slices mean small, focused tests, which is one more reason teams enjoy this style.

References and further reading

Quick recap

  • Layered architecture groups code by technical layer, so one feature is spread across many folders. This gets painful as apps grow.
  • Vertical Slice Architecture groups code by feature. Everything one feature needs lives together in one slice, so changes stay local and safe.
  • FastEndpoints uses the REPR pattern (Request, Endpoint, Response), and each endpoint class is already a vertical slice. The two fit together naturally.
  • Setup is just AddFastEndpoints() and UseFastEndpoints(), and endpoints are discovered automatically.
  • Validation lives right next to its request and runs before your handler, so your logic only sees clean input.
  • You do not need MediatR or MassTransit (both now paid). FastEndpoints has a free, built-in command and event bus to keep slices decoupled.
  • FastEndpoints performs close to Minimal APIs and clearly lighter than MVC controllers, and is production ready on .NET 10.

Related Posts