skills/zeksdev/skills/fast-endpoints

fast-endpoints

SKILL.md

FastEndpoints

High-performance REST API framework for ASP.NET Core using the REPR (Request-Endpoint-Response) pattern.

Quick Start

// Program.cs
var bld = WebApplication.CreateBuilder();
bld.Services.AddFastEndpoints();
var app = bld.Build();
app.UseFastEndpoints();
app.Run();

Endpoint Structure

Basic Endpoint

public class CreateUserRequest
{
    public string Name { get; set; }
    public int Age { get; set; }
}

public class CreateUserResponse
{
    public int Id { get; set; }
    public string FullName { get; set; }
}

public class CreateUserEndpoint : Endpoint<CreateUserRequest, CreateUserResponse>
{
    public override void Configure()
    {
        Post("/api/users");
        AllowAnonymous();
    }

    public override async Task HandleAsync(CreateUserRequest req, CancellationToken ct)
    {
        await SendAsync(new CreateUserResponse
        {
            Id = 1,
            FullName = req.Name
        });
    }
}

Endpoint Base Classes

Base Class Use Case
Endpoint<TRequest> Request only
Endpoint<TRequest, TResponse> Request + Response
EndpointWithoutRequest No DTOs
EndpointWithoutRequest<TResponse> Response only

Fluent alternative: Ep.Req<TRequest>.Res<TResponse>

Attribute-Based Configuration

[HttpPost("/api/users")]
[Authorize(Roles = "Admin")]
[AllowAnonymous]
public class MyEndpoint : Endpoint<MyRequest, MyResponse>
{
    public override Task HandleAsync(MyRequest req, CancellationToken ct) { }
}

Model Binding

Binding priority (highest to lowest): JSON Body → Form → Route → Query → Claims → Headers

Binding Attributes

public class MyRequest
{
    // Route parameter
    public int Id { get; set; }  // Matches route: /api/items/{Id}

    // Explicit binding sources
    [FromHeader] public string TenantId { get; set; }
    [FromClaim] public string UserId { get; set; }
    [QueryParam] public int Page { get; set; }
    [FromBody] public Address Address { get; set; }

    // Name mapping
    [BindFrom("customer_id")] public string CustomerId { get; set; }

    // Permission check
    [HasPermission("Admin")] public bool IsAdmin { get; set; }
}

Manual Parameter Access

public override Task HandleAsync(CancellationToken ct)
{
    var id = Route<int>("id");
    var page = Query<int>("page", isRequired: false);
}

Validation

Uses FluentValidation (included automatically).

public class CreateUserValidator : Validator<CreateUserRequest>
{
    public CreateUserValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Name is required")
            .MinimumLength(3).WithMessage("Name too short");

        RuleFor(x => x.Age)
            .InclusiveBetween(18, 120);
    }
}

In-Handler Validation

public override async Task HandleAsync(MyRequest req, CancellationToken ct)
{
    if (await UserExists(req.Email))
        AddError(r => r.Email, "Email already in use");

    ThrowIfAnyErrors();  // Sends 400 with errors

    // Or immediate abort:
    ThrowError("Something went wrong!");
}

Disable Auto-Validation

public override void Configure()
{
    DontThrowIfValidationFails();
}

public override Task HandleAsync(MyRequest req, CancellationToken ct)
{
    if (ValidationFailed)
    {
        // Handle manually using ValidationFailures
    }
}

Sending Responses

Option 1: Send Methods

// Success responses
await SendAsync(response);                    // 200 OK with body
await SendAsync(response, statusCode);        // Custom status with body
await SendOkAsync(response);                  // Explicit 200 OK
await SendOkAsync();                          // 200 OK without body
await SendNoContentAsync();                   // 204 No Content
await SendCreatedAtAsync<GetEndpoint>(routeValues, response);  // 201 Created

// Error responses
await SendNotFoundAsync();                    // 404 Not Found
await SendNotFoundAsync(response);            // 404 with body
await SendUnauthorizedAsync();                // 401 Unauthorized
await SendForbiddenAsync();                   // 403 Forbidden
await SendErrorsAsync();                      // 400 with ValidationFailures
await SendErrorsAsync(statusCode);            // Custom status with errors

// Redirect
await SendRedirectAsync(url);                 // Redirect response

// File/Stream responses
await SendStreamAsync(stream, fileName, contentType);
await SendFileAsync(fileInfo);
await SendBytesAsync(bytes, fileName, contentType);

Option 2: Response Property

Assign to Response property for automatic 200 OK:

public override async Task HandleAsync(MyRequest req, CancellationToken ct)
{
    // Assign properties
    Response.FullName = req.FirstName + " " + req.LastName;
    Response.Age = req.Age;

    // Or assign new instance
    Response = new MyResponse
    {
        FullName = "john doe",
        Age = 30
    };
    // Response sent automatically at end of HandleAsync
}

Option 3: Conditional Responses with Task

Change return type to Task<Void> to stop execution after sending:

public override async Task<Void> HandleAsync(MyRequest req, CancellationToken ct)
{
    if (req.Id == 0)
        return await SendNotFoundAsync();  // Stops here

    if (!await UserExistsAsync(req.Id))
        return await SendNotFoundAsync("User not found");

    return await SendOkAsync(new MyResponse { Id = req.Id });
}

Option 4: ExecuteAsync with Union Types

Override ExecuteAsync instead of HandleAsync for typed results:

public class GetUserEndpoint : Endpoint<GetUserRequest, Results<Ok<UserResponse>, NotFound, ProblemDetails>>
{
    public override async Task<Results<Ok<UserResponse>, NotFound, ProblemDetails>> ExecuteAsync(
        GetUserRequest req, CancellationToken ct)
    {
        if (req.Id == 0)
            return TypedResults.NotFound();

        var user = await _db.GetUserAsync(req.Id);
        if (user == null)
            return TypedResults.NotFound();

        return TypedResults.Ok(new UserResponse { Id = user.Id, Name = user.Name });
    }
}

Common TypedResults: Ok<T>, NotFound, BadRequest, NoContent, Created<T>, ProblemDetails

ExecuteAsync vs HandleAsync

Method Return Type Use Case
HandleAsync Task or Task<Void> Use Send methods, set Response property
ExecuteAsync Task<TResponse> or Task<Results<...>> Return response directly, union types
// ExecuteAsync returning response directly
public override Task<UserResponse> ExecuteAsync(GetUserRequest req, CancellationToken ct)
{
    return Task.FromResult(new UserResponse { Id = req.Id });
}

Dependency Injection

Property Injection

public class MyEndpoint : Endpoint<MyRequest>
{
    public IUserService UserService { get; set; }  // Auto-injected
}

Constructor Injection

public class MyEndpoint : Endpoint<MyRequest>
{
    private readonly IUserService _userService;

    public MyEndpoint(IUserService userService)
    {
        _userService = userService;
    }
}

Manual Resolution

var service = Resolve<IUserService>();           // Throws if not found
var service = TryResolve<IUserService>();        // Returns null if not found

Pre-Resolved Services

  • ConfigIConfiguration
  • EnvIWebHostEnvironment
  • LoggerILogger

Security

JWT Authentication

// Setup
bld.Services
   .AddAuthenticationJwtBearer(s => s.SigningKey = "your-secret-key")
   .AddAuthorization();

app.UseAuthentication()
   .UseAuthorization()
   .UseFastEndpoints();

// Generate token
var token = JwtBearer.CreateToken(o =>
{
    o.SigningKey = "your-secret-key";
    o.ExpireAt = DateTime.UtcNow.AddHours(1);
    o.User.Roles.Add("Admin");
    o.User.Claims.Add(("UserId", "123"));
    o.User.Permissions.Add("Users.Create");
});

Endpoint Authorization

public override void Configure()
{
    Post("/api/admin/users");

    // Any of these
    Roles("Admin", "Manager");
    Claims("AdminId");
    Permissions("Users.Create");
    Scopes("api:write");
    Policies("AdminOnly");

    // All required
    RolesAll("Admin", "Manager");
    ClaimsAll("AdminId", "TenantId");
    PermissionsAll("Users.Create", "Users.Update");

    // Allow unauthenticated
    AllowAnonymous();

    // Specific auth scheme
    AuthSchemes("Bearer", "ApiKey");
}

Cookie Authentication

bld.Services.AddAuthenticationCookie(validFor: TimeSpan.FromMinutes(30));

// Sign in
await CookieAuth.SignInAsync(u =>
{
    u.Roles.Add("User");
    u.Claims.Add(new("Email", email));
});

// Sign out
await CookieAuth.SignOutAsync();

Pre/Post Processors

Pre-Processor

public class TenantChecker : IPreProcessor<MyRequest>
{
    public Task PreProcessAsync(IPreProcessorContext<MyRequest> ctx, CancellationToken ct)
    {
        if (!ctx.HttpContext.Request.Headers.ContainsKey("X-Tenant-Id"))
        {
            ctx.ValidationFailures.Add(new("TenantId", "Missing tenant header"));
            return ctx.HttpContext.Response.SendErrorsAsync(ctx.ValidationFailures);
        }
        return Task.CompletedTask;
    }
}

public override void Configure()
{
    PreProcessor<TenantChecker>();
}

Post-Processor

public class AuditLogger<TReq, TRes> : IPostProcessor<TReq, TRes>
{
    public Task PostProcessAsync(IPostProcessorContext<TReq, TRes> ctx, CancellationToken ct)
    {
        var logger = ctx.HttpContext.Resolve<ILogger<AuditLogger<TReq, TRes>>>();
        logger.LogInformation("Request completed: {Path}", ctx.HttpContext.Request.Path);
        return Task.CompletedTask;
    }
}

Global Processors

app.UseFastEndpoints(c =>
{
    c.Endpoints.Configurator = ep =>
    {
        ep.PreProcessor<GlobalLogger>(Order.Before);
        ep.PostProcessor<GlobalAudit>(Order.After);
    };
});

Domain Mapping

Separate Mapper Class

public class UserMapper : Mapper<CreateUserRequest, UserResponse, User>
{
    public override User ToEntity(CreateUserRequest r) => new()
    {
        Name = r.Name,
        Email = r.Email
    };

    public override UserResponse FromEntity(User e) => new()
    {
        Id = e.Id,
        FullName = e.Name
    };
}

public class CreateUserEndpoint : Endpoint<CreateUserRequest, UserResponse, UserMapper>
{
    public override async Task HandleAsync(CreateUserRequest req, CancellationToken ct)
    {
        var entity = Map.ToEntity(req);
        // Save entity...
        await SendAsync(Map.FromEntity(entity));
    }
}

Command Bus

// Command definition
public class GetUserQuery : ICommand<UserDto>
{
    public int UserId { get; set; }
}

// Handler
public class GetUserHandler : ICommandHandler<GetUserQuery, UserDto>
{
    public Task<UserDto> ExecuteAsync(GetUserQuery cmd, CancellationToken ct)
    {
        return Task.FromResult(new UserDto { Id = cmd.UserId });
    }
}

// Execute from endpoint
var user = await new GetUserQuery { UserId = 1 }.ExecuteAsync();

Event Bus

// Event
public class UserCreatedEvent
{
    public int UserId { get; set; }
}

// Handler (multiple allowed)
public class SendWelcomeEmail : IEventHandler<UserCreatedEvent>
{
    public Task HandleAsync(UserCreatedEvent e, CancellationToken ct)
    {
        // Send email...
        return Task.CompletedTask;
    }
}

// Publish
await PublishAsync(new UserCreatedEvent { UserId = 1 });
await PublishAsync(evt, Mode.WaitForNone);  // Fire-and-forget

File Handling

public override void Configure()
{
    Post("/api/upload");
    AllowFileUploads();
}

public override async Task HandleAsync(MyRequest req, CancellationToken ct)
{
    foreach (var file in Files)
    {
        using var stream = file.OpenReadStream();
        // Process file...
    }
}

DTO Binding

public class UploadRequest
{
    public IFormFile Document { get; set; }
    public List<IFormFile> Attachments { get; set; }
}

Large Files (Streaming)

public override void Configure()
{
    AllowFileUploads(dontAutoBindFormData: true);
    MaxRequestBodySize(100 * 1024 * 1024);  // 100MB
}

public override async Task HandleAsync(EmptyRequest req, CancellationToken ct)
{
    await foreach (var section in FormFileSectionsAsync(ct))
    {
        using var fs = File.Create(section.FileName);
        await section.Section.Body.CopyToAsync(fs, ct);
    }
}

Swagger/OpenAPI

bld.Services.AddFastEndpoints().SwaggerDocument(o =>
{
    o.DocumentSettings = s =>
    {
        s.Title = "My API";
        s.Version = "v1";
    };
});

app.UseFastEndpoints().UseSwaggerGen();

Endpoint Documentation

public override void Configure()
{
    Summary(s =>
    {
        s.Summary = "Creates a new user";
        s.Description = "Detailed description...";
        s.ExampleRequest = new CreateUserRequest { Name = "John" };
        s.ResponseExamples[200] = new UserResponse { Id = 1 };
        s.Responses[400] = "Validation failed";
    });

    Description(b => b
        .Produces<UserResponse>(200)
        .ProducesProblemDetails(400));
}

Configuration Options

app.UseFastEndpoints(c =>
{
    // Route prefix for all endpoints
    c.Endpoints.RoutePrefix = "api";

    // JSON serialization
    c.Serializer.Options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;

    // Error response format
    c.Errors.UseProblemDetails();

    // Global endpoint settings
    c.Endpoints.Configurator = ep =>
    {
        ep.Description(d => d.WithTags("API"));
    };
});

Testing

See testing.md for integration and unit testing patterns.

API Reference

See api-reference.md for complete method reference.

Weekly Installs
3
Repository
zeksdev/skills
First Seen
Feb 1, 2026
Installed on
kimi-cli3
opencode2
gemini-cli2
claude-code2
github-copilot2
codex2