Background
In modern .NET development, there's been a trend toward minimal APIs - getting rid of controller bloat and focusing on just what matters. But as your app grows, even minimal APIs can become messy and hard to organize.
Carter gives you a clean, modular, and testable way to define your API endpoints - all while still using minimal APIs under the hood. It’s lightweight, composable, and plays nicely with your existing .NET stack.
In this article, we’ll walk through:
• Why Carter is useful
• How to set it up in a .NET 8+ project
• Real-world example with validation and mapping
• Pros & cons of using Carter
• Final thoughts
Why Carter?
Minimal APIs are great for small projects - but as your project grows:
• Routes scatter across multiple files.
• Dependency injection logic is repeated.
• Validation and mapping logic clutters route definitions.
• Testing individual endpoints becomes tricky.
Carter helps you modularize your endpoints into clean, testable components - without falling back to full MVC-style controllers.
Setting Up Carter in .NET
Start by installing the NuGet package:
dotnet add package Carter
Creating a Real-World Carter API
Let’s create a simple Products API where you can:
• Create a product
• Get a list of products
• Use FluentValidation for validation
• Use Custom Mapper for DTO -> Entity mapping
Project Structure
MyApi/
├── Program.cs
├── Endpoints/
│ └── ProductModule.cs
├── Models/
│ ├── Product.cs
│ └── ProductDto.cs
├── Validators/
│ └── CreateProductDtoValidator.cs
Product.cs
namespace MyApi.Models;
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; } = default!;
public decimal Price { get; set; }
}
ProductDto.cs
namespace MyApi.Models;
public record CreateProductDto(string Name, decimal Price);
CreateProductDtoValidator.cs
using FluentValidation;
using MyApi.Models;
namespace MyApi.Validators;
public class CreateProductDtoValidator : AbstractValidator<CreateProductDto>
{
public CreateProductDtoValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
RuleFor(x => x.Price).GreaterThan(0);
}
}
ProductModule.cs
This makes your API modular, clean, and easy to maintain.
Every Carter module implements ICarterModule, which requires a single method: AddRoutes.
You inject services (like validators) directly into handlers.
It all compiles down to regular minimal API endpoints under the hood - just better organized!
using Carter;
using FluentValidation;
using Mapster;
using Microsoft.AspNetCore.Http.HttpResults;
using MyApi.Models;
namespace MyApi.Endpoints;
public class ProductModule : ICarterModule
{
private static readonly List<Product> _products = [];
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapGet("/products", () => _products);
app.MapPost("/products", async (
CreateProductDto dto,
IValidator<CreateProductDto> validator) =>
{
var validationResult = await validator.ValidateAsync(dto);
if (!validationResult.IsValid)
{
var errors = validationResult.Errors
.ToDictionary(e => e.PropertyName, e => e.ErrorMessage);
return Results.BadRequest(errors);
}
var product = dto.ToProduct<Product>();
product.Id = Guid.NewGuid();
_products.Add(product);
return Results.Created($"/products/{product.Id}", product);
});
}
}
Program.cs
That’s it! You now have a clean API with validation and mapping - and all routes are organized into modules.
using Carter;
using FluentValidation;
using MyApi.Models;
using MyApi.Validators;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCarter();
builder.Services.AddScoped<IValidator<CreateProductDto>, CreateProductDtoValidator>();
var app = builder.Build();
app.MapCarter(); // This maps all Carter modules automatically
app.Run();
Why Developers Love Carter
• Keeps things clean and organized
• Grows well with your project
What starts as a small API can get messy fast. Carter gives your project structure, even as it grows.
• You can inject anything you need
• Works great with modern patterns
If you're into vertical slice architecture, CQRS, or minimal APIs - Carter plays really well with those styles.
• Cleaner startup
Your Program.cs stays short and sweet. Just call app.MapCarter() and all your modules are wired up.
• Fast and lightweight
There’s no controller or attribute overhead. It’s just minimal APIs - but more organized.
• Easy to test
Because modules are simple classes, they’re super easy to write tests for - no magic or reflection behind the scenes.
• Flexible and plug-and-play
Things to Keep in Mind
Carter is awesome, but it’s not as mainstream as .NET MVC. You might not find answers on Stack Overflow as quickly.
• You define routes manually
There’s no [HttpGet], [Route], or attribute-based routing here - just regular method calls like MapGet. Some people miss the attributes.
• No fancy model binding attributes
You won’t see [FromBody] or [FromQuery]. Binding works like in minimal APIs - clean, but different if you're used to MVC.
• You have to decide how to structure things
Carter gives you flexibility, but with that comes responsibility. You’ll need to create your own structure for modules, validators, etc.
• No built-in filters or attributes
If you're used to using [Authorize], [ValidateModel], or action filters, you’ll have to wire up that behavior yourself.
Wrapping Up
Carter is one of those libraries that makes you think, “Why didn’t I use this earlier?” It takes the flexibility of minimal APIs and adds just the right amount of structure - without dragging you back into the world of bloated controllers and attributes.
If you're building APIs that are starting to grow beyond a few routes, or you just want a cleaner way to organize your features, Carter is 100% worth trying out.
It’s simple, lightweight, and fits beautifully into modern .NET projects.
Sure, it’s not as mainstream as MVC, and you’ll need to define some structure on your own -nbut that freedom is part of what makes it so powerful.
So the next time you find yourself deep in a messy Program.cs file wondering where to put that new endpoint... maybe give Carter a shot.
You might just fall in love with how clean your code starts to feel.
That's all from me today.
P.S. Follow me on YouTube.