NanoRoute.AwsLambda
NanoRoute.AwsLambda adds AWS Lambda adapters for NanoRoute while keeping the core package transport-neutral.
The package supports Amazon API Gateway HTTP APIs and Lambda Function URLs that invoke Lambda functions with payload format version 2.0. It translates APIGatewayHttpApiV2ProxyRequest events into HttpRequestMessage instances, runs the normal NanoRoute pipeline, and converts the produced HttpResponseMessage into an APIGatewayHttpApiV2ProxyResponse.
NanoRoute.AwsLambda targets net8.0.
Quick Start
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using Microsoft.Extensions.DependencyInjection;
using NanoRoute;
using NanoRoute.AwsLambda;
public sealed class Function
{
// UserRepository is your application service that implements IUserRepository.
private static readonly IServiceProvider Services = new ServiceCollection()
.AddSingleton<IUserRepository, UserRepository>()
.BuildServiceProvider();
private static readonly ApiGatewayV2Router Router = ApiGatewayV2Router
.CreateBuilder()
.AddDefaultValueParsers()
.AddJsonErrorDetails()
.AddEndpoint("GET", "/api/users/{user_id:int}/", endpoint => endpoint
.WithHandler(static async (GetUserRequest request) =>
{
return HttpResponseMessage.Json(HttpStatusCode.OK, new UserResponse
{
Id = request.UserId,
Name = await request.Users.GetNameAsync(request.UserId)
});
}))
.AddEndpoint("POST", "/api/users/", endpoint => endpoint
.WithJsonBody<CreateUserBody>(nameof(CreateUserRequest.Body))
.WithHandler(static async (CreateUserRequest request) =>
{
int userId = await request.Users.CreateAsync(request.Body.Name);
return HttpResponseMessage.Json(HttpStatusCode.Created, new UserResponse
{
Id = userId,
Name = request.Body.Name
});
}))
.CreateRouter();
public Task<APIGatewayHttpApiV2ProxyResponse> FunctionHandler(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)
{
return Router.Route(request, Services, context.RemainingTime);
}
}
public sealed class GetUserRequest
{
[ValueSource(ValueSource.Parameter, Name = "user_id")]
public int UserId { get; set; }
[ValueSource(ValueSource.ServiceLocator)]
public IUserRepository Users { get; set; } = null!;
}
public sealed class CreateUserRequest
{
public CreateUserBody Body { get; set; } = null!;
[ValueSource(ValueSource.ServiceLocator)]
public IUserRepository Users { get; set; } = null!;
}
public sealed class CreateUserBody
{
public string Name { get; set; } = string.Empty;
}
public sealed class UserResponse
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
public interface IUserRepository
{
Task<int> CreateAsync(string name);
Task<string> GetNameAsync(int userId);
}
ApiGatewayV2Router.CreateBuilder() returns the same strongly typed NanoRoute builder style as the core package. Register value parsers, query bindings, JSON body binders, typed handlers, endpoint builders, prefixes, and handlers in the builder, then call CreateRouter() once and reuse the router between Lambda invocations.
Prefer endpoint builders such as AddEndpoint() for application routes. Typed handlers and endpoint helpers such as WithJsonBody() keep route values, JSON bodies, services, and framework values in request objects. AddHandler() is still available for lower-level middleware composition and custom pipelines.
The router entry point accepts the API Gateway request, a service provider, and the Lambda remaining time. Pass ILambdaContext.RemainingTime so the adapter can cancel work shortly before the Lambda runtime terminates the invocation.
For a small working fixture, see Tests/NanoRoute.TestLambda. It wires ApiGatewayV2Router into a Lambda handler and shows endpoint builders, query bindings, JSON body binding, JSON error responses, and cookie mapping in one project.
Core Types
Routing
The Lambda adapter uses the same route matching and handler pipeline as NanoRoute.
using System.Net;
using System.Net.Http;
using NanoRoute;
using NanoRoute.AwsLambda;
ApiGatewayV2Router router = ApiGatewayV2Router
.CreateBuilder()
.AddJsonErrorDetails()
.AddDefaultValueParsers()
.AddPrefix("/api/items/*", items => items
.AddEndpoint("GET", RouteScopeBuilder.CurrentExact, endpoint => endpoint
.WithQueryBindings("{filter?:str(min=3)}")
.WithHandler(static async (context, _) =>
{
await Task.CompletedTask;
return HttpResponseMessage.Json(new
{
filter = context.Parameters.TryGetValue("filter", out object? filter) ? filter : null
});
}))
.AddEndpoint("GET", "/{id:int(min=1)}/", endpoint => endpoint
.WithHandler(static async (context, _) =>
{
await Task.CompletedTask;
return HttpResponseMessage.Json(new
{
id = context.Parameters["id"]
});
})))
.CreateRouter();
CreateBuilder() does not add JSON error handling automatically. Add AddJsonErrorDetails() yourself when you want structured JSON errors, or omit it when you want custom exception handling middleware or response shaping handlers to own failures.
Request Mapping
ApiGatewayV2Router converts the API Gateway event into an HttpRequestMessage before executing the NanoRoute pipeline.
- The request URI is built from
RawPath,RawQueryString, theHostheader, and the request scheme. - The scheme is read from
Forwarded: proto=...,X-Forwarded-Proto, or inferred ashttpsfor Lambda Function URL domains. - Request headers are copied onto the
HttpRequestMessageor its content headers. - Plain request bodies are exposed as
StringContent; base64-encoded request bodies are exposed asStreamContent. - The original
APIGatewayHttpApiV2ProxyRequestis available through the NanoRoute request context as the original request object. - The API Gateway request id is used as the NanoRoute trace id.
Payload format 2.0 does not include the scheme directly, so the adapter derives it from forwarding metadata or the Lambda Function URL domain. If the adapter cannot determine the scheme or host, the request cannot be mapped to an absolute HttpRequestMessage.RequestUri and routing fails before handlers run.
Response Mapping
NanoRoute handlers return ordinary HttpResponseMessage instances.
StringContentis returned as a plain response body.- Other content types are base64 encoded and set
IsBase64Encodedtotrue. - Response headers are flattened into the API Gateway response header dictionary.
Set-Cookievalues are returned through the API GatewayCookiescollection.
Header values other than Set-Cookie are joined with commas, matching the single-value header model used by APIGatewayHttpApiV2ProxyResponse.Headers.
Timeout Handling
Pass ILambdaContext.RemainingTime to Route(). By default, the adapter starts cancellation one second before the Lambda timeout so async value parsers and handlers can observe ValueParserContext.Cancellation and RequestContext.Cancellation.
If the invocation is already inside that safety window, the request is cancelled immediately and the adapter returns a 504 Gateway Timeout JSON error response.
Use ApiGatewayV2RouterConfig.LambdaTimeoutBuffer when your function needs a different safety window:
ApiGatewayV2Router router = ApiGatewayV2Router
.CreateBuilder()
.ConfigureRouting(config => config with
{
LambdaTimeoutBuffer = TimeSpan.FromSeconds(3)
})
.AddJsonErrorDetails()
.AddEndpoint("GET", "/health/", endpoint => endpoint
.WithHandler(static async (_, _) =>
{
await Task.CompletedTask;
return new HttpResponseMessage(HttpStatusCode.OK);
}))
.CreateRouter();
Typed Handlers
Typed handlers work the same way under Lambda as they do in the core package. The service provider passed to Route() is exposed through RequestContext.Services, so [ValueSource(ValueSource.ServiceLocator)] can resolve request object dependencies from your Lambda function's service container.
using System.Net.Http;
using System.Threading;
using NanoRoute;
using NanoRoute.AwsLambda;
public sealed class GetItemRequest
{
public int Id { get; set; }
[ValueSource(ValueSource.Parameter, Name = "filter")]
public string? Filter { get; set; }
[ValueSource(ValueSource.ServiceLocator)]
public IItemRepository Items { get; set; } = null!;
public CancellationToken Cancellation { get; set; }
}
ApiGatewayV2Router router = ApiGatewayV2Router
.CreateBuilder()
.AddJsonErrorDetails()
.AddDefaultValueParsers()
.AddPrefix("/items/{id:int}/*", items => items
.AddEndpoint(["GET"], RouteScopeBuilder.CurrentExact, endpoint => endpoint
.WithQueryBindings("{filter?:str(min=3)}")
.WithHandler(static async (GetItemRequest request) =>
{
Item item = await request.Items.GetAsync(request.Id, request.Filter, request.Cancellation);
return HttpResponseMessage.Json(item);
})))
.CreateRouter();
Common Building Blocks
ApiGatewayV2Router.CreateBuilder()starts a strongly typed builder for API Gateway HTTP API and Lambda Function URL payload-format-2.0 scenarios.ApiGatewayV2RouterConfiginherits the coreRouterConfig, includingMatchingPrecedence, and addsLambdaTimeoutBuffer.ConfigureRouting()customizesApiGatewayV2RouterConfigbefore creating a router snapshot.Route(APIGatewayHttpApiV2ProxyRequest, IServiceProvider, TimeSpan)executes the NanoRoute pipeline and returns an API Gateway v2 proxy response.AddDefaultValueParsers()registers the built-inint,guid,bool, andstrroute parsers.AddQueryBindings()andEndpointBuilder.WithQueryBindings()bind selected query-string values intoRequestContext.Parameters.ConfigureQueryParsing()customizes query-binding behavior used by subsequently registeredAddQueryBindings()andEndpointBuilder.WithQueryBindings()middleware.AddEndpoint()andCreateEndpoint()capture endpoint verbs and route patterns once; endpoint helpers such asWithHandler(),WithJsonBody(), andWithQueryBindings()work under Lambda the same way they do in the core package.AddJsonBody()andEndpointBuilder.WithJsonBody()bind JSON request content intoRequestContext.Parameters.AddJsonErrorDetails()turns routing exceptions into JSONErrorDetailsresponses when explicitly added.ConfigureJsonErrorDetails()customizes JSONErrorDetailsresponse diagnostics and serialization metadata used by subsequently registeredAddJsonErrorDetails()middleware.AddHandler<TRequest>()andEndpointBuilder.WithHandler<TRequest>()projectRequestContextinto a typed request object before invoking the handler.HttpResponseMessage.Json(...)creates JSON responses with the library's serializer defaults.
Supported API Gateway Model
This package is intentionally narrow:
- Supported: API Gateway HTTP API events represented by
APIGatewayHttpApiV2ProxyRequest. - Supported: Lambda Function URL events represented by
APIGatewayHttpApiV2ProxyRequest. - Supported: Lambda proxy responses represented by
APIGatewayHttpApiV2ProxyResponse. - Not currently supported: REST API payload format
1.0, Application Load Balancer events, or custom event models.
For unsupported transports, derive a custom router from the core Router<TDescendant, TConfig> type and map your event model to HttpRequestMessage before calling Handle().