DEV Community

Eelco Los
Eelco Los

Posted on • Edited on

Migrating Azure Function Calls to Minimal API with FastEndpoints

Azure Functions are a popular choice for building serverless, event-driven applications. However, as applications grow in complexity, developers often encounter challenges that require greater control and efficiency. Furthermore, the frequent updates to Azure Functions APIs, including the upcoming deprecation of the in-process worker model in November 2026, make continuous upgrades cumbersome. For business-critical applications, ensuring zero downtime during these transitions can be especially challenging.

In this post, we'll explore how to migrate business-critical Azure Function calls to Minimal API, leveraging FastEndpoints to simplify the transition. While FastEndpoints is the focus, the design concepts discussed here can be applied even without relying on specific packages.


Why Migrate to Minimal API?

Azure Functions are excellent for quick, lightweight serverless operations, but they can present limitations for complex, business-critical applications. Here are some reasons to consider transitioning to Minimal API:

  • Performance Boost: Minimal APIs are optimized for low latency and high throughput.
  • Improved Structure: FastEndpoints offers clear, organized handling of API routes and processes, helping maintain clean separation of concerns.
  • Enhanced Control: Minimal APIs provide developers with fine-grained control over dependency injection, middleware, and other components.

Comparing Azure Functions to Minimal API with FastEndpoints

To illustrate the differences, let’s compare a simple Azure Function to an equivalent implementation using Minimal API and FastEndpoints.

Azure Function Example

[FunctionName("MyFunction")]
public async Task<IActionResult> Run(
    [HttpTrigger(
        AuthorizationLevel.Function,
        "post",
        Route = "demo/{RouteId}/{SubId}")]
        HttpRequest req,
        string RouteId,
        string SubId,
        ILogger log)
{
    log.LogInformation("Processing a request...");

    // Validate RouteId
    if (!Guid.TryParse(RouteId, out Guid routeId))
    {
        return new BadRequestObjectResult(new ProblemDetails
        {
            Title = "Invalid RouteId",
            Status = StatusCodes.Status400BadRequest,
            Detail = "RouteId must be a valid GUID"
        });
    }

    // Validate SubId
    if (!Guid.TryParse(SubId, out Guid subId))
    {
        return new BadRequestObjectResult(new ProblemDetails
        {
            Title = "Invalid SubId",
            Status = StatusCodes.Status400BadRequest,
            Detail = "SubId must be a valid GUID"
        });
    }

    // Extract CompanyId from JWT token
    var tokenHandler = new JwtSecurityTokenHandler();
    var token = req.Headers["Authorization"].ToString().Split(" ")[1];
    var companyId = Guid.Parse(
        tokenHandler.ReadJwtToken(token)
            .Claims.First(c => c.Type == "YourCompanyIdClaim")
            .Value);

    return new OkObjectResult("Hello from Azure Function");
}
Enter fullscreen mode Exit fullscreen mode

This implementation directly interacts with HttpRequest, leaving validation and structure to the developer. While flexible, it lacks the strong typing and modularity of more modern approaches.

Minimal API Example with FastEndpoints

public class MyEndpoint : Endpoint<MyRequest>
{
    public override void Configure()
    {
        // Define the route and HTTP method
        Post("demo/{RouteId}/{SubId}");
    }

    public override async Task HandleAsync(
        MyRequest req, CancellationToken ct)
    {
        // Return a response directly
        await SendOkAsync("Hello from Minimal API", ct);
    }
}

// Model defining expected input
public record MyRequest
{
    Guid RouteId { get; init; }
    Guid SubId { get; init; }

    // Automatically bind claim from JWT token
    [FromClaim("YourCompanyIdClaim")]
    Guid CompanyId { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

Here, the main action happens in the HandleAsync method, while the MyRequest object handles model binding. FastEndpoints simplifies validation and dependency injection, reducing boilerplate code and improving maintainability.


Breaking Down the Key Differences

Comparing the two approaches side by side reveals key differences. Let’s explore how these differences impact structure, developer experience, and flexibility:

Validation

Validation occurs before the main handler in FastEndpoints, ensuring only valid models reach the core logic. For instance:

public class MyRequestValidator : Validator<MyRequest>
{
    public MyRequestValidator()
    {
        RuleFor(x => x.RouteId)
            .NotEmpty().WithMessage("RouteId is required")
            .NotEqual(Guid.Empty).WithMessage("RouteId must be a valid GUID");

        RuleFor(x => x.SubId)
            .NotEmpty().WithMessage("SubId is required")
            .NotEqual(Guid.Empty).WithMessage("SubId must be a valid GUID");
    }
}
Enter fullscreen mode Exit fullscreen mode

This approach improves readability and eliminates repetitive validation code inside the main handler.

Preprocessing Example

public async Task PreProcessAsync(
    IPreProcessorContext<MyRequest> ctx, CancellationToken c)
{
    if (ctx.Request is null)
    {
        await ctx.HttpContext.Response.SendAsync(
            new FastEndpoints.ProblemDetails(ctx.ValidationFailures),
            StatusCodes.Status400BadRequest, cancellation: c);
        return;
    }

    var service = ctx.HttpContext.RequestServices
        .GetRequiredService<ExternalConnectionService>();
    var connection = await service.GetAsync(
        ctx.Request.CompanyId,
        Guid.Parse(ctx.Request.SubId));

    if (connection is null)
    {
        await ctx.HttpContext.Response.SendAsync(
            new FastEndpoints.ProblemDetails([
                new() { ErrorMessage = "No External Connection Found",
                        PropertyName = "subId" }
            ]),
            StatusCodes.Status400BadRequest, cancellation: c);
        return;
    }

    ctx.Request.ExternalConnection = connection;
}
Enter fullscreen mode Exit fullscreen mode

This flexibility is key for handling custom scenarios without compromising structure or clarity.


Conclusion

Migrating from Azure Functions to Minimal API can bring significant benefits, especially for business-critical applications requiring better structure and performance. By leveraging FastEndpoints, you can ensure a smooth transition while maintaining clarity and scalability in your code.

Have you migrated an application from Azure Functions to Minimal API? Share your experience in the comments or try out FastEndpoints to see how it simplifies API development.

Top comments (0)