From 78303f00ca29fd5b9ebd686e0c540270c950879f Mon Sep 17 00:00:00 2001 From: mate329 Date: Mon, 16 Feb 2026 22:31:16 +0100 Subject: [PATCH 1/7] apigw-python-cdk-lambda-snapstart pattern push --- .../CarHandler/handler.py | 125 ++++++++++++++++++ apigw-python-cdk-lambda-snapstart/README.md | 49 +++++++ apigw-python-cdk-lambda-snapstart/app.py | 68 ++++++++++ apigw-python-cdk-lambda-snapstart/cdk.json | 15 +++ .../example-pattern.json | 73 ++++++++++ .../requirements.txt | 2 + 6 files changed, 332 insertions(+) create mode 100644 apigw-python-cdk-lambda-snapstart/CarHandler/handler.py create mode 100644 apigw-python-cdk-lambda-snapstart/README.md create mode 100644 apigw-python-cdk-lambda-snapstart/app.py create mode 100644 apigw-python-cdk-lambda-snapstart/cdk.json create mode 100644 apigw-python-cdk-lambda-snapstart/example-pattern.json create mode 100644 apigw-python-cdk-lambda-snapstart/requirements.txt diff --git a/apigw-python-cdk-lambda-snapstart/CarHandler/handler.py b/apigw-python-cdk-lambda-snapstart/CarHandler/handler.py new file mode 100644 index 000000000..39378ec4e --- /dev/null +++ b/apigw-python-cdk-lambda-snapstart/CarHandler/handler.py @@ -0,0 +1,125 @@ +import base64 +import json +import logging +import os +import uuid +from decimal import Decimal +from typing import Any, Dict, Optional + +import boto3 +from botocore.exceptions import ClientError + +logger = logging.getLogger() +logger.setLevel(os.getenv("LOG_LEVEL", "INFO")) + +table_name = os.environ["CAR_TABLE_NAME"] +dynamodb = boto3.resource("dynamodb") +table = dynamodb.Table(table_name) + +def _json_default(value: Any) -> Any: + if isinstance(value, Decimal): + if value % 1 == 0: + return int(value) + return float(value) + raise TypeError(f"Object of type {type(value)} is not JSON serializable") + +def _response(status_code: int, body: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + payload = "" if body is None else json.dumps(body, default=_json_default) + return { + "statusCode": status_code, + "headers": {"Content-Type": "application/json"}, + "body": payload, + } + +def _parse_body(event: Dict[str, Any]) -> Dict[str, Any]: + raw = event.get("body") or "{}" + if event.get("isBase64Encoded", False): + raw = base64.b64decode(raw).decode("utf-8") + try: + body = json.loads(raw) + except json.JSONDecodeError as exc: + raise ValueError("Invalid JSON body") from exc + + if not isinstance(body, dict): + raise ValueError("Request body must be a JSON object") + + return body + +def _path_parts(path: str) -> list[str]: + if not path: + return [] + return [part for part in path.strip("/").split("/") if part] + +def _create_car(event: Dict[str, Any]) -> Dict[str, Any]: + body = _parse_body(event) + car_id = str(uuid.uuid4()) + car = { + "id": car_id, + "make": body.get("make"), + "model": body.get("model"), + "year": body.get("year"), + "color": body.get("color"), + } + table.put_item(Item=car) + return _response(201, car) + +def _get_car(car_id: str) -> Dict[str, Any]: + item = table.get_item(Key={"id": car_id}).get("Item") + if not item: + return _response(404, {"message": f"Car with id {car_id} not found"}) + return _response(200, item) + +def _update_car(event: Dict[str, Any], car_id: str) -> Dict[str, Any]: + body = _parse_body(event) + + existing = table.get_item(Key={"id": car_id}).get("Item") + if not existing: + return _response(404, {"message": f"Car with id {car_id} not found"}) + + updated = { + "id": car_id, + "make": body.get("make", existing.get("make")), + "model": body.get("model", existing.get("model")), + "year": body.get("year", existing.get("year")), + "color": body.get("color", existing.get("color")), + } + table.put_item(Item=updated) + return _response(200, updated) + + +def _delete_car(car_id: str) -> Dict[str, Any]: + try: + table.delete_item( + Key={"id": car_id}, + ConditionExpression="attribute_exists(id)", + ) + except ClientError as exc: + if exc.response["Error"]["Code"] == "ConditionalCheckFailedException": + return _response(404, {"message": f"Car with id {car_id} not found"}) + raise + + return _response(204) + +def handler(event: Dict[str, Any], _: Any) -> Dict[str, Any]: + method = (event.get("httpMethod") or "").upper() + path = event.get("path") or "" + parts = _path_parts(path) + + logger.info(json.dumps({"method": method, "path": path})) + + try: + if method == "POST" and parts == ["cars"]: + return _create_car(event) + + if len(parts) == 2 and parts[0] == "cars": + car_id = parts[1] + if method == "GET": + return _get_car(car_id) + if method == "PUT": + return _update_car(event, car_id) + if method == "DELETE": + return _delete_car(car_id) + + return _response(404, {"message": "Route not found"}) + except ValueError as exc: + return _response(400, {"message": str(exc)}) diff --git a/apigw-python-cdk-lambda-snapstart/README.md b/apigw-python-cdk-lambda-snapstart/README.md new file mode 100644 index 000000000..28dc40295 --- /dev/null +++ b/apigw-python-cdk-lambda-snapstart/README.md @@ -0,0 +1,49 @@ +# API Gateway + Lambda SnapStart + DynamoDB (Python CDK) + +This pattern demonstrates how to create a REST API using API Gateway, AWS Lambda and DynamoDB. +It's built with [Python 3.12](https://www.python.org/downloads/release/python-3128/), together with +[AWS Cloud Development Kit (CDK)](https://docs.aws.amazon.com/cdk/v2/guide/work-with-cdk-python.html) as the Infrastructure as Code solution. This pattern also implements the usage of [AWS Lambda SnapStart](https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html) +to improve initialization performance of the Lambda, in case that there are libraries requiring more time to load into memory. + +## Architecture + +- API Gateway REST API (`prod` stage) +- AWS Lambda (Python 3.12) +- Lambda SnapStart enabled on published versions +- Lambda `live` alias integrated with API Gateway +- DynamoDB table with partition key `id` + +## Endpoints + +- `POST /cars` +- `GET /cars/{carId}` +- `PUT /cars/{carId}` +- `DELETE /cars/{carId}` + +## Requirements + +- Python 3.12+ +- AWS CDK v2 +- AWS credentials configured + +## Deploy + +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +cdk bootstrap +cdk deploy +``` + +## Test + +Get endpoint URL from stack outputs (`CarEndpoint`), then run: + +```bash +ENDPOINT="" + +curl --location --request POST "$ENDPOINT/cars" \ + --header 'Content-Type: application/json' \ + --data-raw '{"make":"Porsche","model":"992","year":"2022","color":"White"}' +``` \ No newline at end of file diff --git a/apigw-python-cdk-lambda-snapstart/app.py b/apigw-python-cdk-lambda-snapstart/app.py new file mode 100644 index 000000000..8a5cedb2d --- /dev/null +++ b/apigw-python-cdk-lambda-snapstart/app.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +from aws_cdk import ( + App, CfnOutput, + Duration, + Stack, + RemovalPolicy, + aws_apigateway as apigw, + aws_dynamodb as dynamodb, + aws_lambda as _lambda, +) +from constructs import Construct + +class CarStoreStack(Stack): + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + car_table = dynamodb.Table( + self, + "CarTable", + partition_key=dynamodb.Attribute(name="id", type=dynamodb.AttributeType.STRING), + billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, + removal_policy=RemovalPolicy.DESTROY, + ) + + car_function = _lambda.Function( + self, + "CarStoreFunction", + runtime=_lambda.Runtime.PYTHON_3_12, + handler="handler.handler", + code=_lambda.Code.from_asset("CarHandler/"), + timeout=Duration.seconds(10), + snap_start=_lambda.SnapStartConf.ON_PUBLISHED_VERSIONS, + memory_size=256, + environment={ + "CAR_TABLE_NAME": car_table.table_name, + "LOG_LEVEL": "INFO", + } + ) + car_table.grant_read_write_data(car_function) + + live_alias = _lambda.Alias( + self, + "CarStoreLiveAlias", + alias_name="live", + version=car_function.current_version, + ) + + car_api = apigw.RestApi( + self, + "CarStoreApi", + deploy_options=apigw.StageOptions(stage_name="prod"), + ) + + integration = apigw.LambdaIntegration(live_alias, proxy=True) + car_api.root.add_method("ANY", integration) + car_api.root.add_resource("{proxy+}").add_method("ANY", integration) + + CfnOutput( + self, + "CarEndpoint", + description="API Gateway Car Endpoint", + value=car_api.url, + ) + CfnOutput(self, "CarTableName", value=car_table.table_name) + +app = App() +CarStoreStack(app, "CarStoreStack") +app.synth() diff --git a/apigw-python-cdk-lambda-snapstart/cdk.json b/apigw-python-cdk-lambda-snapstart/cdk.json new file mode 100644 index 000000000..daf896a03 --- /dev/null +++ b/apigw-python-cdk-lambda-snapstart/cdk.json @@ -0,0 +1,15 @@ +{ + "app": "../.venv/bin/python app.py", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "**/__pycache__", + "tests" + ] + } +} \ No newline at end of file diff --git a/apigw-python-cdk-lambda-snapstart/example-pattern.json b/apigw-python-cdk-lambda-snapstart/example-pattern.json new file mode 100644 index 000000000..b7fa38ff7 --- /dev/null +++ b/apigw-python-cdk-lambda-snapstart/example-pattern.json @@ -0,0 +1,73 @@ +{ + "title": "API Gateway with Lambda SnapStart and DynamoDB using Python CDK", + "description": "This pattern demonstrates how to create a REST API using API Gateway, AWS Lambda with SnapStart, and DynamoDB. Built with Python 3.12 and AWS CDK, it implements Lambda SnapStart to improve initialization performance for faster cold starts.", + "language": "Python", + "level": "200", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern creates a REST API for managing car records using API Gateway and Lambda with SnapStart enabled.", + "The Lambda function is Python 3.12 based and includes a live alias that's integrated with API Gateway for seamless deployments.", + "Lambda SnapStart persists the initialized state of the Lambda runtime, significantly reducing cold start times for function initialization.", + "DynamoDB stores car records with a partition key of 'id', providing a scalable NoSQL database backend for the REST API." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-python-cdk-lambda-snapstart", + "templateURL": "serverless-patterns/apigw-python-cdk-lambda-snapstart", + "projectFolder": "apigw-python-cdk-lambda-snapstart", + "templateFile": "apigw-python-cdk-lambda-snapstart/app.py" + } + }, + "resources": { + "bullets": [ + { + "text": "AWS Lambda SnapStart", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html" + }, + { + "text": "API Gateway REST API", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html" + }, + { + "text": "AWS CDK Python Reference", + "link": "https://docs.aws.amazon.com/cdk/v2/guide/work-with-cdk-python.html" + } + ] + }, + "deploy": { + "text": [ + "python3 -m venv .venv", + "source .venv/bin/activate", + "pip install -r requirements.txt", + "cdk bootstrap", + "cdk deploy" + ] + }, + "testing": { + "text": [ + "Get the CarEndpoint from stack outputs, then test the endpoint to create a new car record:", + "curl --location --request POST \"$ENDPOINT/cars\" --header 'Content-Type: application/json' --data-raw '{\"make\":\"Porsche\",\"model\":\"992\",\"year\":\"2022\",\"color\":\"White\"}'", + "Change the endpoint and HTTP method to test other operations:", + "GET /cars/{carId} - Retrieve a car", + "PUT /cars/{carId} - Update a car", + "DELETE /cars/{carId} - Delete a car" + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk destroy." + ] + }, + "authors": [ + { + "name": "Matia Rasetina", + "image": "https://media.licdn.com/dms/image/v2/C4D03AQEpZLzvymfGyA/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1612951581132?e=1772668800&v=beta&t=m8AkoSUFICMRk5-Gd0hEAji0N4gFSfFGuv4lbBuXcJY", + "bio": "Senior Software Engineer @ Elixirr Digital", + "linkedin": "https://www.linkedin.com/in/matiarasetina/", + "twitter": "" + } + ] +} \ No newline at end of file diff --git a/apigw-python-cdk-lambda-snapstart/requirements.txt b/apigw-python-cdk-lambda-snapstart/requirements.txt new file mode 100644 index 000000000..7b6a920fd --- /dev/null +++ b/apigw-python-cdk-lambda-snapstart/requirements.txt @@ -0,0 +1,2 @@ +aws-cdk-lib>=2.170.0 +constructs>=10.0.0,<11.0.0 \ No newline at end of file From 54d5a5540a9c0564189172c97666559b0825d8cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matia=20Ra=C5=A1etina?= <45571679+mate329@users.noreply.github.com> Date: Sat, 14 Mar 2026 11:18:00 +0100 Subject: [PATCH 2/7] Update apigw-python-cdk-lambda-snapstart/README.md Co-authored-by: ellisms <114107920+ellisms@users.noreply.github.com> --- apigw-python-cdk-lambda-snapstart/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-python-cdk-lambda-snapstart/README.md b/apigw-python-cdk-lambda-snapstart/README.md index 28dc40295..82a58a6f8 100644 --- a/apigw-python-cdk-lambda-snapstart/README.md +++ b/apigw-python-cdk-lambda-snapstart/README.md @@ -1,4 +1,4 @@ -# API Gateway + Lambda SnapStart + DynamoDB (Python CDK) +# Amazon API Gateway + AWS Lambda SnapStart + Amazon DynamoDB This pattern demonstrates how to create a REST API using API Gateway, AWS Lambda and DynamoDB. It's built with [Python 3.12](https://www.python.org/downloads/release/python-3128/), together with From a275c993e9b25d13e5ecfcc8f6e997a224c1e942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matia=20Ra=C5=A1etina?= <45571679+mate329@users.noreply.github.com> Date: Sat, 14 Mar 2026 11:18:10 +0100 Subject: [PATCH 3/7] Update apigw-python-cdk-lambda-snapstart/README.md Co-authored-by: ellisms <114107920+ellisms@users.noreply.github.com> --- apigw-python-cdk-lambda-snapstart/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-python-cdk-lambda-snapstart/README.md b/apigw-python-cdk-lambda-snapstart/README.md index 82a58a6f8..7dd348759 100644 --- a/apigw-python-cdk-lambda-snapstart/README.md +++ b/apigw-python-cdk-lambda-snapstart/README.md @@ -1,6 +1,6 @@ # Amazon API Gateway + AWS Lambda SnapStart + Amazon DynamoDB -This pattern demonstrates how to create a REST API using API Gateway, AWS Lambda and DynamoDB. +This pattern demonstrates how to create a REST API using Amazon API Gateway, AWS Lambda and Amazon DynamoDB. It's built with [Python 3.12](https://www.python.org/downloads/release/python-3128/), together with [AWS Cloud Development Kit (CDK)](https://docs.aws.amazon.com/cdk/v2/guide/work-with-cdk-python.html) as the Infrastructure as Code solution. This pattern also implements the usage of [AWS Lambda SnapStart](https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html) to improve initialization performance of the Lambda, in case that there are libraries requiring more time to load into memory. From cd74c2f1ecb006a51977184923bb2ff00f54772e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matia=20Ra=C5=A1etina?= <45571679+mate329@users.noreply.github.com> Date: Sat, 14 Mar 2026 11:18:18 +0100 Subject: [PATCH 4/7] Update apigw-python-cdk-lambda-snapstart/README.md Co-authored-by: ellisms <114107920+ellisms@users.noreply.github.com> --- apigw-python-cdk-lambda-snapstart/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-python-cdk-lambda-snapstart/README.md b/apigw-python-cdk-lambda-snapstart/README.md index 7dd348759..7466badc9 100644 --- a/apigw-python-cdk-lambda-snapstart/README.md +++ b/apigw-python-cdk-lambda-snapstart/README.md @@ -3,7 +3,7 @@ This pattern demonstrates how to create a REST API using Amazon API Gateway, AWS Lambda and Amazon DynamoDB. It's built with [Python 3.12](https://www.python.org/downloads/release/python-3128/), together with [AWS Cloud Development Kit (CDK)](https://docs.aws.amazon.com/cdk/v2/guide/work-with-cdk-python.html) as the Infrastructure as Code solution. This pattern also implements the usage of [AWS Lambda SnapStart](https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html) -to improve initialization performance of the Lambda, in case that there are libraries requiring more time to load into memory. +to improve initialization performance of the Lambda function. ## Architecture From 23d3967a07e18f68cb6ec4723b7548057818ad8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matia=20Ra=C5=A1etina?= <45571679+mate329@users.noreply.github.com> Date: Sat, 14 Mar 2026 11:18:25 +0100 Subject: [PATCH 5/7] Update apigw-python-cdk-lambda-snapstart/README.md Co-authored-by: ellisms <114107920+ellisms@users.noreply.github.com> --- apigw-python-cdk-lambda-snapstart/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-python-cdk-lambda-snapstart/README.md b/apigw-python-cdk-lambda-snapstart/README.md index 7466badc9..fe7a7f9ec 100644 --- a/apigw-python-cdk-lambda-snapstart/README.md +++ b/apigw-python-cdk-lambda-snapstart/README.md @@ -8,7 +8,7 @@ to improve initialization performance of the Lambda function. ## Architecture - API Gateway REST API (`prod` stage) -- AWS Lambda (Python 3.12) +- AWS Lambda functon(Python 3.12) - Lambda SnapStart enabled on published versions - Lambda `live` alias integrated with API Gateway - DynamoDB table with partition key `id` From 91b3a2f5c17ffa073fead33acfbbebcba9e15ceb Mon Sep 17 00:00:00 2001 From: mate329 Date: Sat, 14 Mar 2026 12:13:40 +0100 Subject: [PATCH 6/7] implement aws-lambda-powertools inside the CarHandler Lambda as suggested --- .../CarHandler/handler.py | 143 ++++++++++-------- .../CarHandler/requirements.txt | 1 + 2 files changed, 80 insertions(+), 64 deletions(-) create mode 100644 apigw-python-cdk-lambda-snapstart/CarHandler/requirements.txt diff --git a/apigw-python-cdk-lambda-snapstart/CarHandler/handler.py b/apigw-python-cdk-lambda-snapstart/CarHandler/handler.py index 39378ec4e..c266f4d9c 100644 --- a/apigw-python-cdk-lambda-snapstart/CarHandler/handler.py +++ b/apigw-python-cdk-lambda-snapstart/CarHandler/handler.py @@ -1,16 +1,24 @@ -import base64 import json -import logging import os import uuid from decimal import Decimal -from typing import Any, Dict, Optional +from typing import Any import boto3 from botocore.exceptions import ClientError -logger = logging.getLogger() -logger.setLevel(os.getenv("LOG_LEVEL", "INFO")) +from aws_lambda_powertools import Logger +from aws_lambda_powertools.event_handler import ( + APIGatewayRestResolver, + Response, + content_types, +) +from aws_lambda_powertools.event_handler.exceptions import BadRequestError, NotFoundError +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +logger = Logger(level=os.getenv("LOG_LEVEL", "INFO")) +app = APIGatewayRestResolver() table_name = os.environ["CAR_TABLE_NAME"] dynamodb = boto3.resource("dynamodb") @@ -23,35 +31,20 @@ def _json_default(value: Any) -> Any: return float(value) raise TypeError(f"Object of type {type(value)} is not JSON serializable") -def _response(status_code: int, body: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - payload = "" if body is None else json.dumps(body, default=_json_default) - return { - "statusCode": status_code, - "headers": {"Content-Type": "application/json"}, - "body": payload, - } - -def _parse_body(event: Dict[str, Any]) -> Dict[str, Any]: - raw = event.get("body") or "{}" - if event.get("isBase64Encoded", False): - raw = base64.b64decode(raw).decode("utf-8") - try: - body = json.loads(raw) - except json.JSONDecodeError as exc: - raise ValueError("Invalid JSON body") from exc - - if not isinstance(body, dict): - raise ValueError("Request body must be a JSON object") - return body +def _json_body() -> dict: + """Parse request body as JSON object; empty or missing body returns {}.""" + raw = app.current_event.json_body + if raw is None: + return {} + if not isinstance(raw, dict): + raise BadRequestError("Request body must be a JSON object") + return raw -def _path_parts(path: str) -> list[str]: - if not path: - return [] - return [part for part in path.strip("/").split("/") if part] -def _create_car(event: Dict[str, Any]) -> Dict[str, Any]: - body = _parse_body(event) +@app.post("/cars") +def create_car() -> Response: + body = _json_body() car_id = str(uuid.uuid4()) car = { "id": car_id, @@ -61,21 +54,31 @@ def _create_car(event: Dict[str, Any]) -> Dict[str, Any]: "color": body.get("color"), } table.put_item(Item=car) - return _response(201, car) + return Response( + status_code=201, + content_type=content_types.APPLICATION_JSON, + body=json.dumps(car, default=_json_default), + ) + -def _get_car(car_id: str) -> Dict[str, Any]: +@app.get("/cars/") +def get_car(car_id: str) -> Response: item = table.get_item(Key={"id": car_id}).get("Item") if not item: - return _response(404, {"message": f"Car with id {car_id} not found"}) - return _response(200, item) + raise NotFoundError(f"Car with id {car_id} not found") + return Response( + status_code=200, + content_type=content_types.APPLICATION_JSON, + body=json.dumps(item, default=_json_default), + ) -def _update_car(event: Dict[str, Any], car_id: str) -> Dict[str, Any]: - body = _parse_body(event) +@app.put("/cars/") +def update_car(car_id: str) -> Response: + body = _json_body() existing = table.get_item(Key={"id": car_id}).get("Item") if not existing: - return _response(404, {"message": f"Car with id {car_id} not found"}) - + raise NotFoundError(f"Car with id {car_id} not found") updated = { "id": car_id, "make": body.get("make", existing.get("make")), @@ -84,10 +87,15 @@ def _update_car(event: Dict[str, Any], car_id: str) -> Dict[str, Any]: "color": body.get("color", existing.get("color")), } table.put_item(Item=updated) - return _response(200, updated) + return Response( + status_code=200, + content_type=content_types.APPLICATION_JSON, + body=json.dumps(updated, default=_json_default), + ) -def _delete_car(car_id: str) -> Dict[str, Any]: +@app.delete("/cars/") +def delete_car(car_id: str) -> Response: try: table.delete_item( Key={"id": car_id}, @@ -95,31 +103,38 @@ def _delete_car(car_id: str) -> Dict[str, Any]: ) except ClientError as exc: if exc.response["Error"]["Code"] == "ConditionalCheckFailedException": - return _response(404, {"message": f"Car with id {car_id} not found"}) + raise NotFoundError(f"Car with id {car_id} not found") from exc raise + return Response(status_code=204, body="") - return _response(204) -def handler(event: Dict[str, Any], _: Any) -> Dict[str, Any]: - method = (event.get("httpMethod") or "").upper() - path = event.get("path") or "" - parts = _path_parts(path) +@app.exception_handler(NotFoundError) +def handle_not_found(exc: NotFoundError) -> Response: + return Response( + status_code=404, + content_type=content_types.APPLICATION_JSON, + body=json.dumps({"message": str(exc)}), + ) - logger.info(json.dumps({"method": method, "path": path})) - try: - if method == "POST" and parts == ["cars"]: - return _create_car(event) - - if len(parts) == 2 and parts[0] == "cars": - car_id = parts[1] - if method == "GET": - return _get_car(car_id) - if method == "PUT": - return _update_car(event, car_id) - if method == "DELETE": - return _delete_car(car_id) - - return _response(404, {"message": "Route not found"}) - except ValueError as exc: - return _response(400, {"message": str(exc)}) +@app.exception_handler(BadRequestError) +def handle_bad_request(exc: BadRequestError) -> Response: + return Response( + status_code=400, + content_type=content_types.APPLICATION_JSON, + body=json.dumps({"message": str(exc)}), + ) + + +@app.not_found +def handle_route_not_found(_exc: Exception) -> Response: + return Response( + status_code=404, + content_type=content_types.APPLICATION_JSON, + body=json.dumps({"message": "Route not found"}), + ) + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) +def handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/apigw-python-cdk-lambda-snapstart/CarHandler/requirements.txt b/apigw-python-cdk-lambda-snapstart/CarHandler/requirements.txt new file mode 100644 index 000000000..b8cad7ceb --- /dev/null +++ b/apigw-python-cdk-lambda-snapstart/CarHandler/requirements.txt @@ -0,0 +1 @@ +aws-lambda-powertools==3.25.0 \ No newline at end of file From 1f171a0ad9925bfe9e2ce973c9c085df16de6d8d Mon Sep 17 00:00:00 2001 From: mate329 Date: Sat, 14 Mar 2026 12:14:46 +0100 Subject: [PATCH 7/7] cdk.json bugfix + add additional cURL calls for other CRUD operations + clearer instructions for deployment --- apigw-python-cdk-lambda-snapstart/README.md | 25 ++++++++++++++++++++- apigw-python-cdk-lambda-snapstart/cdk.json | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/apigw-python-cdk-lambda-snapstart/README.md b/apigw-python-cdk-lambda-snapstart/README.md index fe7a7f9ec..8dc862d54 100644 --- a/apigw-python-cdk-lambda-snapstart/README.md +++ b/apigw-python-cdk-lambda-snapstart/README.md @@ -28,10 +28,21 @@ to improve initialization performance of the Lambda function. ## Deploy +To deploy this stack, run the following commands from the root of the `serverless-patterns` repository: + ```bash +# Move to the pattern directory and create a Python virtual environment +cd apigw-python-cdk-lambda-snapstart python3 -m venv .venv source .venv/bin/activate -pip install -r requirements.txt + +# Install the AWS CDK for Python +pip3 install -r requirements.txt + +# Install AWS Lambda Powertools library for the CarHandler Lambda +pip3 install -r CarHandler/requirements.txt -t CarHandler/ + +# Bootstrap your environment and deploy cdk bootstrap cdk deploy ``` @@ -43,7 +54,19 @@ Get endpoint URL from stack outputs (`CarEndpoint`), then run: ```bash ENDPOINT="" +# Create a car (use the returned "id" in the response for GET/PUT/DELETE below) curl --location --request POST "$ENDPOINT/cars" \ --header 'Content-Type: application/json' \ --data-raw '{"make":"Porsche","model":"992","year":"2022","color":"White"}' + +# Get a car by id +curl --location "$ENDPOINT/cars/" + +# Update a car +curl --location --request PUT "$ENDPOINT/cars/" \ + --header 'Content-Type: application/json' \ + --data-raw '{"make":"Porsche","model":"992","year":"2023","color":"Racing Yellow"}' + +# Delete a car +curl --location --request DELETE "$ENDPOINT/cars/" ``` \ No newline at end of file diff --git a/apigw-python-cdk-lambda-snapstart/cdk.json b/apigw-python-cdk-lambda-snapstart/cdk.json index daf896a03..c8091b7f4 100644 --- a/apigw-python-cdk-lambda-snapstart/cdk.json +++ b/apigw-python-cdk-lambda-snapstart/cdk.json @@ -1,5 +1,5 @@ { - "app": "../.venv/bin/python app.py", + "app": ".venv/bin/python app.py", "watch": { "include": [ "**"