AWSTemplateFormatVersion:"2010-09-09"Transform:AWS::Serverless-2016-10-31Description:Hello world event handler API GatewayGlobals:Api:TracingEnabled:trueCors:# see CORS sectionAllowOrigin:"'https://example.com'"AllowHeaders:"'Content-Type,Authorization,X-Amz-Date'"MaxAge:"'300'"BinaryMediaTypes:# see Binary responses section-"*~1*"# converts to */* for any binary typeFunction:Timeout:5Runtime:python3.9Tracing:ActiveEnvironment:Variables:LOG_LEVEL:INFOPOWERTOOLS_LOGGER_SAMPLE_RATE:0.1POWERTOOLS_LOGGER_LOG_EVENT:truePOWERTOOLS_SERVICE_NAME:exampleResources:ApiFunction:Type:AWS::Serverless::FunctionProperties:Handler:getting_started_rest_api_resolver.lambda_handlerCodeUri:../srcDescription:API handler functionEvents:AnyApiEvent:Type:ApiProperties:# NOTE: this is a catch-all rule to simplify the documentation.# explicit routes and methods are recommended for prod instead (see below)Path:/{proxy+}# Send requests on any path to the lambda functionMethod:ANY# Send requests using any http method to the lambda function# GetAllTodos:# Type: Api# Properties:# Path: /todos# Method: GET# GetTodoById:# Type: Api# Properties:# Path: /todos/{todo_id}# Method: GET# CreateTodo:# Type: Api# Properties:# Path: /todos# Method: POST
AWSTemplateFormatVersion:"2010-09-09"Transform:AWS::Serverless-2016-10-31Description:Hello world event handler Lambda Function URLGlobals:Function:Timeout:5Runtime:python3.9Tracing:ActiveEnvironment:Variables:LOG_LEVEL:INFOPOWERTOOLS_LOGGER_SAMPLE_RATE:0.1POWERTOOLS_LOGGER_LOG_EVENT:truePOWERTOOLS_SERVICE_NAME:exampleFunctionUrlConfig:Cors:# see CORS section# Notice that values here are Lists of Strings, vs comma-separated values on API GatewayAllowOrigins:["https://example.com"]AllowHeaders:["Content-Type","Authorization","X-Amz-Date"]MaxAge:300Resources:ApiFunction:Type:AWS::Serverless::FunctionProperties:Handler:getting_started_lambda_function_url_resolver.lambda_handlerCodeUri:../srcDescription:API handler functionFunctionUrlConfig:AuthType:NONE# AWS_IAM for added security beyond sample documentation
Before you decorate your functions to handle a given path and HTTP method(s), you need to initialize a resolver.
A resolver will handle request resolution, including one or more routers, and give you access to the current event via typed properties.
For resolvers, we provide: APIGatewayRestResolver, APIGatewayHttpResolver, ALBResolver, and LambdaFunctionUrlResolver. From here on, we will default to APIGatewayRestResolver across examples.
Auto-serialization
We serialize Dict responses as JSON, trim whitespace for compact responses, and set content-type to application/json.
importrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportAPIGatewayRestResolverfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()@app.get("/todos")@tracer.capture_methoddefget_todos():todos:Response=requests.get("https://jsonplaceholder.typicode.com/todos")todos.raise_for_status()# for brevity, we'll limit to the first 10 onlyreturn{"todos":todos.json()[:10]}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
This utility uses path and httpMethod to route to the right function. This helps make unit tests and local invocation easier too.
{"body":"","resource":"/todos","path":"/todos","httpMethod":"GET","isBase64Encoded":false,"queryStringParameters":{},"multiValueQueryStringParameters":{},"pathParameters":{},"stageVariables":{},"headers":{"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8","Accept-Encoding":"gzip, deflate, sdch","Accept-Language":"en-US,en;q=0.8","Cache-Control":"max-age=0","CloudFront-Forwarded-Proto":"https","CloudFront-Is-Desktop-Viewer":"true","CloudFront-Is-Mobile-Viewer":"false","CloudFront-Is-SmartTV-Viewer":"false","CloudFront-Is-Tablet-Viewer":"false","CloudFront-Viewer-Country":"US","Host":"1234567890.execute-api.us-east-1.amazonaws.com","Upgrade-Insecure-Requests":"1","User-Agent":"Custom User Agent String","Via":"1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)","X-Amz-Cf-Id":"cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==","X-Forwarded-For":"127.0.0.1, 127.0.0.2","X-Forwarded-Port":"443","X-Forwarded-Proto":"https"},"multiValueHeaders":{},"requestContext":{"accountId":"123456789012","resourceId":"123456","stage":"Prod","requestId":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef","requestTime":"25/Jul/2020:12:34:56 +0000","requestTimeEpoch":1428582896000,"identity":{"cognitoIdentityPoolId":null,"accountId":null,"cognitoIdentityId":null,"caller":null,"accessKey":null,"sourceIp":"127.0.0.1","cognitoAuthenticationType":null,"cognitoAuthenticationProvider":null,"userArn":null,"userAgent":"Custom User Agent String","user":null},"path":"/Prod/todos","resourcePath":"/todos","httpMethod":"GET","apiId":"1234567890","protocol":"HTTP/1.1"}}
12345678
{"statusCode":200,"multiValueHeaders":{"Content-Type":["application/json"]},"body":"{\"todos\":[{\"userId\":1,\"id\":1,\"title\":\"delectus aut autem\",\"completed\":false},{\"userId\":1,\"id\":2,\"title\":\"quis ut nam facilis et officia qui\",\"completed\":false},{\"userId\":1,\"id\":3,\"title\":\"fugiat veniam minus\",\"completed\":false},{\"userId\":1,\"id\":4,\"title\":\"et porro tempora\",\"completed\":true},{\"userId\":1,\"id\":5,\"title\":\"laboriosam mollitia et enim quasi adipisci quia provident illum\",\"completed\":false},{\"userId\":1,\"id\":6,\"title\":\"qui ullam ratione quibusdam voluptatem quia omnis\",\"completed\":false},{\"userId\":1,\"id\":7,\"title\":\"illo expedita consequatur quia in\",\"completed\":false},{\"userId\":1,\"id\":8,\"title\":\"quo adipisci enim quam ut ab\",\"completed\":true},{\"userId\":1,\"id\":9,\"title\":\"molestiae perspiciatis ipsa\",\"completed\":false},{\"userId\":1,\"id\":10,\"title\":\"illo est ratione doloremque quia maiores aut\",\"completed\":true}]}","isBase64Encoded":false}
importrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportAPIGatewayHttpResolverfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayHttpResolver()@app.get("/todos")@tracer.capture_methoddefget_todos():todos:Response=requests.get("https://jsonplaceholder.typicode.com/todos")todos.raise_for_status()# for brevity, we'll limit to the first 10 onlyreturn{"todos":todos.json()[:10]}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
importrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportALBResolverfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=ALBResolver()@app.get("/todos")@tracer.capture_methoddefget_todos():todos:Response=requests.get("https://jsonplaceholder.typicode.com/todos")todos.raise_for_status()# for brevity, we'll limit to the first 10 onlyreturn{"todos":todos.json()[:10]}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPLICATION_LOAD_BALANCER)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
importrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportLambdaFunctionUrlResolverfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=LambdaFunctionUrlResolver()@app.get("/todos")@tracer.capture_methoddefget_todos():todos:Response=requests.get("https://jsonplaceholder.typicode.com/todos")todos.raise_for_status()# for brevity, we'll limit to the first 10 onlyreturn{"todos":todos.json()[:10]}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.LAMBDA_FUNCTION_URL)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
{"version":"2.0","routeKey":"$default","rawPath":"/todos","rawQueryString":"","headers":{"x-amz-content-sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","x-amzn-tls-version":"TLSv1.2","x-amz-date":"20220803T092917Z","x-forwarded-proto":"https","x-forwarded-port":"443","x-forwarded-for":"123.123.123.123","accept":"application/xml","x-amzn-tls-cipher-suite":"ECDHE-RSA-AES128-GCM-SHA256","x-amzn-trace-id":"Root=1-63ea3fee-51ba94542feafa3928745ba3","host":"xxxxxxxxxxxxx.lambda-url.eu-central-1.on.aws","content-type":"application/json","accept-encoding":"gzip, deflate","user-agent":"Custom User Agent"},"requestContext":{"accountId":"123457890","apiId":"xxxxxxxxxxxxxxxxxxxx","authorizer":{"iam":{"accessKey":"AAAAAAAAAAAAAAAAAA","accountId":"123457890","callerId":"AAAAAAAAAAAAAAAAAA","cognitoIdentity":null,"principalOrgId":"o-xxxxxxxxxxxx","userArn":"arn:aws:iam::AAAAAAAAAAAAAAAAAA:user/user","userId":"AAAAAAAAAAAAAAAAAA"}},"domainName":"xxxxxxxxxxxxx.lambda-url.eu-central-1.on.aws","domainPrefix":"xxxxxxxxxxxxx","http":{"method":"GET","path":"/todos","protocol":"HTTP/1.1","sourceIp":"123.123.123.123","userAgent":"Custom User Agent"},"requestId":"24f9ef37-8eb7-45fe-9dbc-a504169fd2f8","routeKey":"$default","stage":"$default","time":"03/Aug/2022:09:29:18 +0000","timeEpoch":1659518958068},"isBase64Encoded":false}
You can use /todos/<todo_id> to configure dynamic URL paths, where <todo_id> will be resolved at runtime.
Each dynamic route you set must be part of your function signature. This allows us to call your function using keyword arguments when matching your dynamic route.
Note
For brevity, we will only include the necessary keys for each sample request for the example to work.
importrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportAPIGatewayRestResolverfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()@app.get("/todos/<todo_id>")@tracer.capture_methoddefget_todo_by_id(todo_id:str):# value come as strtodos:Response=requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}")todos.raise_for_status()return{"todos":todos.json()}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
We recommend having explicit routes whenever possible; use catch-all routes sparingly.
You can use a regex string to handle an arbitrary number of paths within a request, for example .+.
You can also combine nested paths with greedy regex to catch in between routes.
Warning
We choose the most explicit registered route that matches an incoming event.
1 2 3 4 5 6 7 8 9101112131415161718192021
fromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportAPIGatewayRestResolverfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()@app.get(".+")@tracer.capture_methoddefcatch_any_route_get_method():return{"path_received":app.current_event.path}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
You can use named decorators to specify the HTTP method that should be handled in your functions. That is, app.<http_method>, where the HTTP method could be get, post, put, patch, delete, and options.
importrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportAPIGatewayRestResolverfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()@app.post("/todos")@tracer.capture_methoddefcreate_todo():todo_data:dict=app.current_event.json_body# deserialize json str to dicttodo:Response=requests.post("https://jsonplaceholder.typicode.com/todos",data=todo_data)todo.raise_for_status()return{"todo":todo.json()}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
importrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportAPIGatewayRestResolverfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()# PUT and POST HTTP requests to the path /hello will route to this function@app.route("/todos",method=["PUT","POST"])@tracer.capture_methoddefcreate_todo():todo_data:dict=app.current_event.json_body# deserialize json str to dicttodo:Response=requests.post("https://jsonplaceholder.typicode.com/todos",data=todo_data)todo.raise_for_status()return{"todo":todo.json()}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
Note
It is generally better to have separate functions for each HTTP method, as the functionality tends to differ depending on which method is used.
Event Handler integrates with Event Source Data Classes utilities, and it exposes their respective resolver request details and convenient methods under app.current_event.
That is why you see app.resolve(event, context) in every example. This allows Event Handler to resolve requests, and expose data like app.lambda_context and app.current_event.
Within app.current_event property, you can access all available query strings as a dictionary via query_string_parameters, or a specific one via get_query_string_value method.
You can access the raw payload via body property, or if it's a JSON string you can quickly deserialize it via json_body property - like the earlier example in the HTTP Methods section.
fromtypingimportOptionalimportrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportAPIGatewayRestResolverfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()@app.get("/todos")@tracer.capture_methoddefget_todos():todo_id:str=app.current_event.get_query_string_value(name="id",default_value="")# alternatively_:Optional[str]=app.current_event.query_string_parameters.get("id")# Payload_:Optional[str]=app.current_event.body# raw str | Noneendpoint="https://jsonplaceholder.typicode.com/todos"iftodo_id:endpoint=f"{endpoint}/{todo_id}"todos:Response=requests.get(endpoint)todos.raise_for_status()return{"todos":todos.json()}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
importrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportAPIGatewayRestResolverfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()@app.get("/todos")@tracer.capture_methoddefget_todos():endpoint="https://jsonplaceholder.typicode.com/todos"api_key:str=app.current_event.get_header_value(name="X-Api-Key",case_sensitive=True,default_value="")todos:Response=requests.get(endpoint,headers={"X-Api-Key":api_key})todos.raise_for_status()return{"todos":todos.json()}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
importrequestsfromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimport(APIGatewayRestResolver,Response,content_types,)fromaws_lambda_powertools.event_handler.exceptionsimportNotFoundErrorfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()@app.not_found@tracer.capture_methoddefhandle_not_found_errors(exc:NotFoundError)->Response:logger.info(f"Not found route: {app.current_event.path}")returnResponse(status_code=418,content_type=content_types.TEXT_PLAIN,body="I'm a teapot!")@app.get("/todos")@tracer.capture_methoddefget_todos():todos:requests.Response=requests.get("https://jsonplaceholder.typicode.com/todos")todos.raise_for_status()# for brevity, we'll limit to the first 10 onlyreturn{"todos":todos.json()[:10]}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
You can use exception_handler decorator with any Python exception. This allows you to handle a common exception outside your route, for example validation errors.
importrequestsfromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimport(APIGatewayRestResolver,Response,content_types,)fromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()@app.exception_handler(ValueError)defhandle_invalid_limit_qs(ex:ValueError):# receives exception raisedmetadata={"path":app.current_event.path,"query_strings":app.current_event.query_string_parameters}logger.error(f"Malformed request: {ex}",extra=metadata)returnResponse(status_code=400,content_type=content_types.TEXT_PLAIN,body="Invalid request parameters.",)@app.get("/todos")@tracer.capture_methoddefget_todos():# educational purpose only: we should receive a `ValueError`# if a query string value for `limit` cannot be coerced to intmax_results:int=int(app.current_event.get_query_string_value(name="limit",default_value=0))todos:requests.Response=requests.get(f"https://jsonplaceholder.typicode.com/todos?limit={max_results}")todos.raise_for_status()return{"todos":todos.json()}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
You can easily raise any HTTP Error back to the client using ServiceError exception. This ensures your Lambda function doesn't fail but return the correct HTTP response signalling the error.
Info
If you need to send custom headers, use Response class instead.
We provide pre-defined errors for the most popular ones such as HTTP 400, 401, 404, 500.
importrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportAPIGatewayRestResolverfromaws_lambda_powertools.event_handler.exceptionsimport(BadRequestError,InternalServerError,NotFoundError,ServiceError,UnauthorizedError,)fromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()@app.get(rule="/bad-request-error")defbad_request_error():raiseBadRequestError("Missing required parameter")# HTTP 400@app.get(rule="/unauthorized-error")defunauthorized_error():raiseUnauthorizedError("Unauthorized")# HTTP 401@app.get(rule="/not-found-error")defnot_found_error():raiseNotFoundError# HTTP 404@app.get(rule="/internal-server-error")definternal_server_error():raiseInternalServerError("Internal server error")# HTTP 500@app.get(rule="/service-error",cors=True)defservice_error():raiseServiceError(502,"Something went wrong!")@app.get("/todos")@tracer.capture_methoddefget_todos():todos:Response=requests.get("https://jsonplaceholder.typicode.com/todos")todos.raise_for_status()return{"todos":todos.json()[:10]}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
Scenario: You have a custom domain api.mydomain.dev. Then you set /payment API Mapping to forward any payment requests to your Payments API.
Challenge: This means your path value for any API requests will always contain /payment/<actual_request>, leading to HTTP 404 as Event Handler is trying to match what's after payment/. This gets further complicated with an arbitrary level of nesting.
To address this API Gateway behavior, we use strip_prefixes parameter to account for these prefixes that are now injected into the path regardless of which type of API Gateway you're using.
After removing a path prefix with strip_prefixes, the new root path will automatically be mapped to the path argument of /.
For example, when using strip_prefixes value of /pay, there is no difference between a request path of /pay and /pay/; and the path argument would be defined as /.
importrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportAPIGatewayRestResolver,CORSConfigfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()cors_config=CORSConfig(allow_origin="https://example.com",max_age=300)app=APIGatewayRestResolver(cors=cors_config)@app.get("/todos")@tracer.capture_methoddefget_todos():todos:Response=requests.get("https://jsonplaceholder.typicode.com/todos")todos.raise_for_status()# for brevity, we'll limit to the first 10 onlyreturn{"todos":todos.json()[:10]}@app.get("/todos/<todo_id>")@tracer.capture_methoddefget_todo_by_id(todo_id:str):# value come as strtodos:Response=requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}")todos.raise_for_status()return{"todos":todos.json()}@app.get("/healthcheck",cors=False)# optionally removes CORS for a given route@tracer.capture_methoddefam_i_alive():return{"am_i_alive":"yes"}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
1 2 3 4 5 6 7 8 910
{"statusCode":200,"multiValueHeaders":{"Content-Type":["application/json"],"Access-Control-Allow-Origin":["https://www.example.com"],"Access-Control-Allow-Headers":["Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token,X-Api-Key"]},"body":"{\"todos\":[{\"userId\":1,\"id\":1,\"title\":\"delectus aut autem\",\"completed\":false},{\"userId\":1,\"id\":2,\"title\":\"quis ut nam facilis et officia qui\",\"completed\":false},{\"userId\":1,\"id\":3,\"title\":\"fugiat veniam minus\",\"completed\":false},{\"userId\":1,\"id\":4,\"title\":\"et porro tempora\",\"completed\":true},{\"userId\":1,\"id\":5,\"title\":\"laboriosam mollitia et enim quasi adipisci quia provident illum\",\"completed\":false},{\"userId\":1,\"id\":6,\"title\":\"qui ullam ratione quibusdam voluptatem quia omnis\",\"completed\":false},{\"userId\":1,\"id\":7,\"title\":\"illo expedita consequatur quia in\",\"completed\":false},{\"userId\":1,\"id\":8,\"title\":\"quo adipisci enim quam ut ab\",\"completed\":true},{\"userId\":1,\"id\":9,\"title\":\"molestiae perspiciatis ipsa\",\"completed\":false},{\"userId\":1,\"id\":10,\"title\":\"illo est ratione doloremque quia maiores aut\",\"completed\":true}]}","isBase64Encoded":false}
Pre-flight (OPTIONS) calls are typically handled at the API Gateway or Lambda Function URL level as per our sample infrastructure, no Lambda integration is necessary. However, ALB expects you to handle pre-flight requests.
You can use the Response class to have full control over the response. For example, you might want to add additional headers, cookies, or set a custom Content-type.
Info
Powertools serializes headers and cookies according to the type of input event.
Some event sources require headers and cookies to be encoded as multiValueHeaders.
fromhttpimportHTTPStatusfromuuidimportuuid4importrequestsfromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimport(APIGatewayRestResolver,Response,content_types,)fromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.shared.cookiesimportCookiefromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()@app.get("/todos")@tracer.capture_methoddefget_todos():todos:requests.Response=requests.get("https://jsonplaceholder.typicode.com/todos")todos.raise_for_status()custom_headers={"X-Transaction-Id":[f"{uuid4()}"]}returnResponse(status_code=HTTPStatus.OK.value,# 200content_type=content_types.APPLICATION_JSON,body=todos.json()[:10],headers=custom_headers,cookies=[Cookie(name="session_id",value="12345")],)# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
1 2 3 4 5 6 7 8 910
{"statusCode":200,"multiValueHeaders":{"Content-Type":["application/json"],"X-Transaction-Id":["3490eea9-791b-47a0-91a4-326317db61a9"],"Set-Cookie":["session_id=12345; Secure"]},"body":"{\"todos\":[{\"userId\":1,\"id\":1,\"title\":\"delectus aut autem\",\"completed\":false},{\"userId\":1,\"id\":2,\"title\":\"quis ut nam facilis et officia qui\",\"completed\":false},{\"userId\":1,\"id\":3,\"title\":\"fugiat veniam minus\",\"completed\":false},{\"userId\":1,\"id\":4,\"title\":\"et porro tempora\",\"completed\":true},{\"userId\":1,\"id\":5,\"title\":\"laboriosam mollitia et enim quasi adipisci quia provident illum\",\"completed\":false},{\"userId\":1,\"id\":6,\"title\":\"qui ullam ratione quibusdam voluptatem quia omnis\",\"completed\":false},{\"userId\":1,\"id\":7,\"title\":\"illo expedita consequatur quia in\",\"completed\":false},{\"userId\":1,\"id\":8,\"title\":\"quo adipisci enim quam ut ab\",\"completed\":true},{\"userId\":1,\"id\":9,\"title\":\"molestiae perspiciatis ipsa\",\"completed\":false},{\"userId\":1,\"id\":10,\"title\":\"illo est ratione doloremque quia maiores aut\",\"completed\":true}]}","isBase64Encoded":false}
importrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportAPIGatewayRestResolverfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()@app.get("/todos",compress=True)@tracer.capture_methoddefget_todos():todos:Response=requests.get("https://jsonplaceholder.typicode.com/todos")todos.raise_for_status()# for brevity, we'll limit to the first 10 onlyreturn{"todos":todos.json()[:10]}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
For convenience, we automatically base64 encode binary responses. You can also use in combination with compress parameter if your client supports gzip.
Like compress feature, the client must send the Accept header with the correct media type.
Warning
This feature requires API Gateway to configure binary media types, see our sample infrastructure for reference.
Note
Lambda Function URLs handle binary media types automatically.
importosfrompathlibimportPathfromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handler.api_gatewayimport(APIGatewayRestResolver,Response,)fromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()logo_file:bytes=Path(f"{os.getenv('LAMBDA_TASK_ROOT')}/logo.svg").read_bytes()@app.get("/logo")@tracer.capture_methoddefget_logo():returnResponse(status_code=200,content_type="image/svg+xml",body=logo_file)# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
importrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportAPIGatewayRestResolverfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver(debug=True)@app.get("/todos")@tracer.capture_methoddefget_todos():todos:Response=requests.get("https://jsonplaceholder.typicode.com/todos")todos.raise_for_status()# for brevity, we'll limit to the first 10 onlyreturn{"todos":todos.json()[:10]}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
importjsonfromdataclassesimportasdict,dataclass,is_dataclassfromjsonimportJSONEncoderimportrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportAPIGatewayRestResolverfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()@dataclassclassTodo:userId:strid:str# noqa: A003 VNE003 "id" field is reservedtitle:strcompleted:boolclassDataclassCustomEncoder(JSONEncoder):"""A custom JSON encoder to serialize dataclass obj"""defdefault(self,obj):# Only called for values that aren't JSON serializable# where `obj` will be an instance of Todo in this examplereturnasdict(obj)ifis_dataclass(obj)elsesuper().default(obj)defcustom_serializer(obj)->str:"""Your custom serializer function APIGatewayRestResolver will use"""returnjson.dumps(obj,separators=(",",":"),cls=DataclassCustomEncoder)app=APIGatewayRestResolver(serializer=custom_serializer)@app.get("/todos")@tracer.capture_methoddefget_todos():ret:Response=requests.get("https://jsonplaceholder.typicode.com/todos")ret.raise_for_status()todos=[Todo(**todo)fortodoinret.json()]# for brevity, we'll limit to the first 10 onlyreturn{"todos":todos[:10]}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
As you grow the number of routes a given Lambda function should handle, it is natural to split routes into separate files to ease maintenance - That's where the Router feature is useful.
Let's assume you have split_route.py as your Lambda function entrypoint and routes in split_route_module.py. This is how you'd use the Router feature.
We import Router instead of APIGatewayRestResolver; syntax wise is exactly the same.
importrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportTracerfromaws_lambda_powertools.event_handler.api_gatewayimportRoutertracer=Tracer()router=Router()endpoint="https://jsonplaceholder.typicode.com/todos"@router.get("/todos")@tracer.capture_methoddefget_todos():api_key:str=router.current_event.get_header_value(name="X-Api-Key",case_sensitive=True,default_value="")todos:Response=requests.get(endpoint,headers={"X-Api-Key":api_key})todos.raise_for_status()# for brevity, we'll limit to the first 10 onlyreturn{"todos":todos.json()[:10]}@router.get("/todos/<todo_id>")@tracer.capture_methoddefget_todo_by_id(todo_id:str):# value come as strapi_key:str=router.current_event.get_header_value(name="X-Api-Key",case_sensitive=True,default_value="")# type: ignore[assignment] # sentinel typing # noqa: E501todos:Response=requests.get(f"{endpoint}/{todo_id}",headers={"X-Api-Key":api_key})todos.raise_for_status()return{"todos":todos.json()}
We use include_router method and include all user routers registered in the router global object.
1 2 3 4 5 6 7 8 9101112131415161718
importsplit_route_modulefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportAPIGatewayRestResolverfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()app.include_router(split_route_module.router)# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
In the previous example, split_route_module.py routes had a /todos prefix. This might grow over time and become repetitive.
When necessary, you can set a prefix when including a router object. This means you could remove /todos prefix altogether.
1 2 3 4 5 6 7 8 910111213141516171819
importsplit_route_modulefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportAPIGatewayRestResolverfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()# prefix '/todos' to any route in `split_route_module.router`app.include_router(split_route_module.router,prefix="/todos")# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
importrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportTracerfromaws_lambda_powertools.event_handler.api_gatewayimportRoutertracer=Tracer()router=Router()endpoint="https://jsonplaceholder.typicode.com/todos"@router.get("/")@tracer.capture_methoddefget_todos():api_key:str=router.current_event.get_header_value(name="X-Api-Key",case_sensitive=True,default_value="")todos:Response=requests.get(endpoint,headers={"X-Api-Key":api_key})todos.raise_for_status()# for brevity, we'll limit to the first 10 onlyreturn{"todos":todos.json()[:10]}@router.get("/<todo_id>")@tracer.capture_methoddefget_todo_by_id(todo_id:str):# value come as strapi_key:str=router.current_event.get_header_value(name="X-Api-Key",case_sensitive=True,default_value="")# type: ignore[assignment] # sentinel typing # noqa: E501todos:Response=requests.get(f"{endpoint}/{todo_id}",headers={"X-Api-Key":api_key})todos.raise_for_status()return{"todos":todos.json()}# many more routes
You can use append_context when you want to share data between your App and Router instances. Any data you share will be available via the context dictionary available in your App or Router context.
Info
For safety, we always clear any data available in the context dictionary after each invocation.
Tip
This can also be useful for middlewares injecting contextual information before a request is processed.
1 2 3 4 5 6 7 8 910111213141516171819
importsplit_route_append_context_modulefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportAPIGatewayRestResolverfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()app.include_router(split_route_append_context_module.router)# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:app.append_context(is_admin=True)# arbitrary number of key=value datareturnapp.resolve(event,context)
1 2 3 4 5 6 7 8 910111213141516171819202122232425
importrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportTracerfromaws_lambda_powertools.event_handler.api_gatewayimportRoutertracer=Tracer()router=Router()endpoint="https://jsonplaceholder.typicode.com/todos"@router.get("/todos")@tracer.capture_methoddefget_todos():is_admin:bool=router.context.get("is_admin",False)todos={}ifis_admin:todos:Response=requests.get(endpoint)todos.raise_for_status()todos=todos.json()[:10]# for brevity, we'll limit to the first 10 onlyreturn{"todos":todos}
This is a sample project layout for a monolithic function with routes split in different files (/todos, /health).
Sample project layout
1 2 3 4 5 6 7 8 9101112131415161718192021222324
.
├── pyproject.toml # project app & dev dependencies; poetry, pipenv, etc.
├── poetry.lock
├── src
│ ├── __init__.py
│ ├── requirements.txt # sam build detect it automatically due to CodeUri: src. poetry export --format src/requirements.txt│ └── todos
│ ├── __init__.py
│ ├── main.py # this will be our todos Lambda fn; it could be split in folders if we want separate fns same code base│ └── routers # routers module│ ├── __init__.py
│ ├── health.py # /health routes. from routers import todos; health.router│ └── todos.py # /todos routes. from .routers import todos; todos.router├── template.yml # SAM. CodeUri: src, Handler: todos.main.lambda_handler
└── tests
├── __init__.py
├── unit
│ ├── __init__.py
│ └── test_todos.py # unit tests for the todos router
│ └── test_health.py # unit tests for the health router
└── functional
├── __init__.py
├── conftest.py # pytest fixtures for the functional tests
└── test_main.py # functional tests for the main lambda handler
This utility is optimized for fast startup, minimal feature set, and to quickly on-board customers familiar with frameworks like Flask — it's not meant to be a fully fledged framework.
Event Handler naturally leads to a single Lambda function handling multiple routes for a given service, which can be eventually broken into multiple functions.
Both single (monolithic) and multiple functions (micro) offer different set of trade-offs worth knowing.
Tip
TL;DR. Start with a monolithic function, add additional functions with new handlers, and possibly break into micro functions if necessary.
A monolithic function means that your final code artifact will be deployed to a single function. This is generally the best approach to start.
Benefits
Code reuse. It's easier to reason about your service, modularize it and reuse code as it grows. Eventually, it can be turned into a standalone library.
No custom tooling. Monolithic functions are treated just like normal Python packages; no upfront investment in tooling.
Faster deployment and debugging. Whether you use all-at-once, linear, or canary deployments, a monolithic function is a single deployable unit. IDEs like PyCharm and VSCode have tooling to quickly profile, visualize, and step through debug any Python package.
Downsides
Cold starts. Frequent deployments and/or high load can diminish the benefit of monolithic functions depending on your latency requirements, due to Lambda scaling model. Always load test to pragmatically balance between your customer experience and development cognitive load.
Granular security permissions. The micro function approach enables you to use fine-grained permissions & access controls, separate external dependencies & code signing at the function level. Conversely, you could have multiple functions while duplicating the final code artifact in a monolithic approach.
Regardless, least privilege can be applied to either approaches.
Higher risk per deployment. A misconfiguration or invalid import can cause disruption if not caught earlier in automated testing. Multiple functions can mitigate misconfigurations but they would still share the same code artifact. You can further minimize risks with multiple environments in your CI/CD pipeline.
A micro function means that your final code artifact will be different to each function deployed. This is generally the approach to start if you're looking for fine-grain control and/or high load on certain parts of your service.
Benefits
Granular scaling. A micro function can benefit from the Lambda scaling model to scale differently depending on each part of your application. Concurrency controls and provisioned concurrency can also be used at a granular level for capacity management.
Discoverability. Micro functions are easier do visualize when using distributed tracing. Their high-level architectures can be self-explanatory, and complexity is highly visible — assuming each function is named to the business purpose it serves.
Package size. An independent function can be significant smaller (KB vs MB) depending on external dependencies it require to perform its purpose. Conversely, a monolithic approach can benefit from Lambda Layers to optimize builds for external dependencies.
Downsides
Upfront investment. You need custom build tooling to bundle assets, including C bindings for runtime compatibility. Operations become more elaborate — you need to standardize tracing labels/annotations, structured logging, and metrics to pinpoint root causes.
Engineering discipline is necessary for both approaches. Micro-function approach however requires further attention in consistency as the number of functions grow, just like any distributed system.
Harder to share code. Shared code must be carefully evaluated to avoid unnecessary deployments when that changes. Equally, if shared code isn't a library,
your development, building, deployment tooling need to accommodate the distinct layout.
Slower safe deployments. Safely deploying multiple functions require coordination — AWS CodeDeploy deploys and verifies each function sequentially. This increases lead time substantially (minutes to hours) depending on the deployment strategy you choose. You can mitigate it by selectively enabling it in prod-like environments only, and where the risk profile is applicable.
Automated testing, operational and security reviews are essential to stability in either approaches.
importrequestsfromrequestsimportResponsefromaws_lambda_powertoolsimportLogger,Tracerfromaws_lambda_powertools.event_handlerimportAPIGatewayRestResolverfromaws_lambda_powertools.loggingimportcorrelation_pathsfromaws_lambda_powertools.utilities.typingimportLambdaContexttracer=Tracer()logger=Logger()app=APIGatewayRestResolver()@app.get("/todos")@tracer.capture_methoddefget_todos():todos:Response=requests.get("https://jsonplaceholder.typicode.com/todos")todos.raise_for_status()return{"todos":todos.json()[:10]}# You can continue to use other utilities just as before@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)@tracer.capture_lambda_handlerdeflambda_handler(event:dict,context:LambdaContext)->dict:returnapp.resolve(event,context)
What's the difference between this utility and frameworks like Chalice?
Chalice is a full featured microframework that manages application and infrastructure. This utility, however, is largely focused on routing to reduce boilerplate and expects you to setup and manage infrastructure with your framework of choice.
It's been superseded by more explicit resolvers like APIGatewayRestResolver, APIGatewayHttpResolver, and ALBResolver.
ApiGatewayResolver handled multiple types of event resolvers for convenience via proxy_type param. However,
it made it impossible for static checkers like Mypy and IDEs IntelliSense to know what properties a current_event would have due to late bound resolution.
This provided a suboptimal experience for customers not being able to find all properties available besides common ones between API Gateway REST, HTTP, and ALB - while manually annotating app.current_event would work it is not the experience we want to provide to customers.
ApiGatewayResolver will be deprecated in v2 and have appropriate warnings as soon as we have a v2 draft.