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.
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.
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.
Here is the same idea written as a simple comparison.
| Question | Layered Architecture | Vertical Slice Architecture |
|---|---|---|
| How is code grouped? | By technical layer | By feature |
| Where does one feature live? | Spread across many folders | Together in one slice |
| To add a feature you... | Edit many shared files | Add a new slice |
| Risk of breaking other features | Higher (shared services) | Lower (isolated slices) |
| Easy for new people to read? | Often no | Usually yes |
| Best for | Small or stable apps | Apps 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
Steps
Client sends request
JSON over HTTP
Bind to Request DTO
fill the request object
Validate input
rules run automatically
Run handler logic
HandleAsync does the work
Return Response DTO
send JSON back
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 FastEndpointsNow 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
Steps
Features
top-level folder
Products
one feature area
CreateProduct.cs
one full slice
GetProduct.cs
another full slice
A typical tree looks like this:
ShopApi/
Program.cs
Features/
Products/
CreateProduct.cs
GetProduct.cs
Product.cs # the shared domain modelNotice 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.
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.
| Point | Controllers (MVC) | Minimal APIs | FastEndpoints |
|---|---|---|---|
| Natural fit for VSA | Weak (fat controllers) | Okay (needs discipline) | Strong (one class per slice) |
| Boilerplate per endpoint | High | Low | Low |
| Built-in validation | Manual wiring | Manual wiring | Built in (FluentValidation) |
| Built-in command/event bus | No | No | Yes (free) |
| Performance vs controllers | Baseline | Faster, lighter | Faster, lighter |
| Auto endpoint discovery | No | No | Yes |
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.
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
Productmodel 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, notProductController.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
Productsfolder, anOrdersfolder, 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
- FastEndpoints official documentation — the best place to learn the framework, including validation, the command bus, and the event bus.
- FastEndpoints on GitHub — source code, samples, and release notes.
- Productive Web API Development with FastEndpoints and Vertical Slice Architecture in .NET — a deeper community walkthrough by Anton Martyniuk.
- Getting Started with FastEndpoints — a gentle intro to the basics before VSA.
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()andUseFastEndpoints(), 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
Getting Started with FastEndpoints for Building Web APIs in .NET
A friendly beginner guide to FastEndpoints in .NET. Learn the REPR pattern, build your first endpoint, add validation, and see how it compares to controllers.
Automatically Register Minimal APIs in ASP.NET Core
Learn to auto-register Minimal API endpoints in ASP.NET Core using the IEndpoint pattern, assembly scanning, and source generators. With diagrams and code.
Best Practices for Building REST APIs in ASP.NET Core
A friendly, beginner guide to REST API best practices in ASP.NET Core with naming, status codes, validation, ProblemDetails, paging, versioning, security, and code.
Top 15 Mistakes Developers Make When Creating Web APIs
A warm, beginner-friendly tour of the 15 most common Web API mistakes in ASP.NET Core, with simple fixes, diagrams, tables, and clear C# examples.
How to Increase the Performance of Web APIs in .NET
A friendly, step-by-step guide to making your ASP.NET Core Web APIs fast: async, caching, query tuning, compression, and pooling in .NET 10.
Authentication and Authorization Best Practices in ASP.NET Core
A friendly guide to authentication and authorization in ASP.NET Core for .NET 10 — JWT, cookies, claims, roles, policies, and security best practices with diagrams.