Endpoints
At the very heart of any API are its endpoints. They are the doors through which the outside world communicates with your application. The way you structure these doors has a profound impact on how clean, maintainable, testable, and scalable your application will be.
In traditional web API frameworks, it’s common to use the Model-View-Controller (MVC) pattern, where a single “Controller” class handles multiple, often unrelated, actions. A UsersController
might handle listing users, getting a user by ID, creating a user, updating a user, and deleting a user. While this works for small applications, these controller classes can quickly become bloated, monolithic beasts that are difficult to understand, modify, and test.
SliceFlow, by building on the excellent FastEndpoints library, takes a fundamentally different and more modern approach.
The Philosophy: Do One Thing, and Do It Well
Instead of large controllers, SliceFlow applications are built from many small, focused endpoints. Each endpoint is its own self-contained class that is responsible for handling exactly one action.
This approach is guided by the REPR (Request-Endpoint-Response) Pattern. It’s a simple yet powerful idea where a complete feature is often composed of several small, focused classes.
The Anatomy of a SliceFlow Endpoint: A Complete Example
Let’s break down what a complete, production-ready endpoint looks like. We’ll use the example of an endpoint that assigns permissions to a user.
1. The Request: Defining the Input Contract
First, we define the exact shape of the data the endpoint expects. This is a strongly-typed record, which makes the contract clear and immutable.
public record AssignPermissionRequest([property: BindFrom("id")] Guid UserId, IEnumerable<string> Permissions);
2. The Validator: The Automatic Gatekeeper
For every request, we can create a corresponding validator. SliceFlow uses FluentValidation to define a set of rules in a clean, declarative way. The naming convention (RequestName
+ Validator
) allows the framework to automatically discover and execute it.
public class AssignPermissionRequestValidator : Validator<AssignPermissionRequest>{ public AssignPermissionRequestValidator() { RuleFor(x => x.Permissions).NotEmpty(); }}
This validator ensures that no request with an empty list of permissions ever reaches your business logic. The framework automatically rejects it with a standardized 400 Bad Request
and a ProblemDetails
response. For a deeper dive, see the Validation documentation.
3. The Response: Defining the Output Contract
Next, we define the shape of the data we’ll return on a successful operation. This ensures the client knows exactly what to expect.
public record AssignPermissionResponse(Guid UserId, IEnumerable<string> Permissions);
4. The Endpoint: Where the Work Happens
This is the core class where the configuration and business logic live. It’s lean and focused on its single responsibility.
internal sealed class AssignPermissions(DataContext db, HybridCache cache) : Endpoint<AssignPermissionRequest, Results<Created<AssignPermissionResponse>, NotFound, BadRequest<ProblemDetails>>>{ // Part A: Configuration public override void Configure() { Post("/{userId}/permissions"); Version(1); Group<EndpointGroup.User>(); Permissions(Allow.User.AssignPermissions); }
// Part B: The Logic public override async Task<Results<Created<AssignPermissionResponse>, NotFound, BadRequest<ProblemDetails>>> ExecuteAsync(AssignPermissionRequest req, CancellationToken ct) { // ... business logic to find the user, validate permissions, // ... save changes, and clear the cache ...
var response = new AssignPermissionResponse(user.Id, validPermissions.Select(f => f.Name));
return TypedResults.Created("/users/" + user.Id + "permissions", response); }}
5. The Summary: Describing Your Endpoint
Finally, to make your API discoverable and easy to use, you can provide a summary. This class provides the metadata needed to generate rich OpenAPI (Swagger) documentation.
internal sealed class AssignPermissionsSummary : Summary<AssignPermissions>{ public AssignPermissionsSummary() { Summary = "Assign Permissions"; Description = "This endpoint assigns the given permission to the given user id."; ExampleRequest = new AssignPermissionRequest(Guid.CreateVersion7(), ["User.AssignPermissions"]); }}
Why This Pattern Is a Game-Changer
This endpoint-centric approach, where each feature is a small collection of focused classes, brings a host of benefits:
- Single Responsibility: Each class has one job, making the code dramatically easier to reason about, maintain, and debug.
- Strong Typing Everywhere: You get compile-time safety and powerful IDE assistance from the request all the way to the response.
- Automatic & Tidy: Features like validation and API documentation are handled automatically by the framework based on the classes you define, keeping your endpoint logic clean.
- Effortless Testability: Small, focused classes are a dream to unit test.
- Seamless Integration: This pattern is the foundation upon which other SliceFlow features like Permissions and Logging are built.
By moving away from monolithic controllers to the REPR pattern, SliceFlow helps you build APIs that are not only powerful and performant but also clean, scalable, and a pleasure to develop.