Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions rest-api-dynamodb/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# REST API + DynamoDB

Independent AWS CDK Python example that builds a REST API with five independent
Lambda handlers backed by a real DynamoDB table named `Orders`.
Independent AWS CDK Python example that builds a REST API backed by a real
DynamoDB table named `Orders`. The five functions are grouped by resource in
`lambdas/orders.py`, while shared runtime code is provided by the `orders`
AWS Lambda Layer.

## Architecture

The stack contains one API Gateway REST API, one on-demand DynamoDB table with
string partition key `id`, and one Lambda per route:
string partition key `id`, and five independent Lambda functions. Each
function declares exactly one route:

| Method | Path | Access |
| --- | --- | --- |
Expand All @@ -16,6 +19,30 @@ string partition key `id`, and one Lambda per route:
| PUT | `/orders/{id}` | DynamoDB write |
| DELETE | `/orders/{id}` | DynamoDB write |

The handlers import `orders_shared` from the `orders` Layer through the
standard Lambda `/opt/python` import root. `@layer("orders")` associates every
function with that Layer; the Layer does not grant DynamoDB permissions.

## Project layout

```text
rest-api-dynamodb/
├── lambdas/
│ ├── __init__.py
│ ├── orders.py
│ └── requirements.txt
└── layers/
└── orders/
├── requirements.txt
└── python/
└── orders_shared.py
```

`lambdas/orders.py` contains `list_orders`, `get_order`, `create_order`,
`update_order`, and `delete_order`. The CDK application passes both
`lambda_path="lambdas"` and `layers_path="layers"` to `LambdaApi`, so the
published Layer directory is discovered and attached to each function.

The POST function demonstrates named configuration with Python 3.12, 1024 MB,
15 seconds, `STAGE` and `TABLE_NAME`, the mutable `api-role`, and the function
name `configured-handler`. Other functions use independent CDK-created roles
Expand Down
32 changes: 0 additions & 32 deletions rest-api-dynamodb/lambdas/create_order.py

This file was deleted.

15 changes: 0 additions & 15 deletions rest-api-dynamodb/lambdas/delete_order.py

This file was deleted.

13 changes: 0 additions & 13 deletions rest-api-dynamodb/lambdas/get_order.py

This file was deleted.

11 changes: 0 additions & 11 deletions rest-api-dynamodb/lambdas/list_orders.py

This file was deleted.

103 changes: 76 additions & 27 deletions rest-api-dynamodb/lambdas/orders.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,87 @@
import json
import os
from lambda_api_decorators import (
DELETE,
GET,
POST,
PUT,
description,
environment,
grant_dynamodb,
layer,
memory_size,
name,
role,
runtime,
timeout,
)

import boto3
from orders_shared import order_id, request_json, response, table


JSON_HEADERS = {"Content-Type": "application/json"}
REQUIRED_FIELDS = ("customer", "total", "status")
@GET("/orders")
@layer("orders")
@grant_dynamodb("orders", "read")
@environment("TABLE_NAME")
def list_orders(event, context):
items = table().scan().get("Items", [])
return response(200, items)


def table():
return boto3.resource("dynamodb").Table(os.environ["TABLE_NAME"])
@GET("/orders/{id}")
@layer("orders")
@grant_dynamodb("orders", "read")
@environment("TABLE_NAME")
def get_order(event, context):
item = table().get_item(Key={"id": order_id(event)}).get("Item")
if item is None:
return response(404, {"error": "Order not found"})
return response(200, item)


def response(status_code, payload):
return {
"statusCode": status_code,
"headers": JSON_HEADERS,
"body": "" if status_code == 204 else json.dumps(payload),
}
@POST("/orders")
@layer("orders")
@grant_dynamodb("orders", "write")
@memory_size(1024)
@timeout(15)
@environment("STAGE", "TABLE_NAME")
@runtime("python3.12")
@role("api-role")
@description("Configured endpoint")
@name("configured-handler")
def create_order(event, context):
order, error = request_json(event)
if error:
return error
if "id" not in order:
return response(400, {"error": "Missing required fields", "fields": ["id"]})
table().put_item(Item=order, ConditionExpression="attribute_not_exists(id)")
return response(201, order)


def request_json(event):
try:
body = json.loads(event.get("body") or "")
except (TypeError, json.JSONDecodeError):
return None, response(400, {"error": "Invalid JSON body"})
if not isinstance(body, dict):
return None, response(400, {"error": "JSON body must be an object"})
return body, None
@PUT("/orders/{id}")
@layer("orders")
@grant_dynamodb("orders", "write")
@environment("TABLE_NAME")
def update_order(event, context):
identifier = order_id(event)
order, error = request_json(event)
if error:
return error
existing = table().get_item(Key={"id": identifier}).get("Item")
if existing is None:
return response(404, {"error": "Order not found"})
updated = {"id": identifier, **order}
table().put_item(Item=updated)
return response(200, updated)


def order_id(event):
return (event.get("pathParameters") or {}).get("id")


def missing_fields(order):
return [field for field in REQUIRED_FIELDS if field not in order]
@DELETE("/orders/{id}")
@layer("orders")
@grant_dynamodb("orders", "write")
@environment("TABLE_NAME")
def delete_order(event, context):
deleted = table().delete_item(
Key={"id": order_id(event)}, ReturnValues="ALL_OLD"
).get("Attributes")
if deleted is None:
return response(404, {"error": "Order not found"})
return response(204, None)
19 changes: 0 additions & 19 deletions rest-api-dynamodb/lambdas/update_order.py

This file was deleted.

38 changes: 38 additions & 0 deletions rest-api-dynamodb/layers/orders/python/orders_shared.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import json
import os

import boto3


JSON_HEADERS = {"Content-Type": "application/json"}
REQUIRED_FIELDS = ("customer", "total", "status")


def table():
return boto3.resource("dynamodb").Table(os.environ["TABLE_NAME"])


def response(status_code, payload):
return {
"statusCode": status_code,
"headers": JSON_HEADERS,
"body": "" if status_code == 204 else json.dumps(payload),
}


def request_json(event):
try:
body = json.loads(event.get("body") or "")
except (TypeError, json.JSONDecodeError):
return None, response(400, {"error": "Invalid JSON body"})
if not isinstance(body, dict):
return None, response(400, {"error": "JSON body must be an object"})
return body, None


def order_id(event):
return (event.get("pathParameters") or {}).get("id")


def missing_fields(order):
return [field for field in REQUIRED_FIELDS if field not in order]
1 change: 1 addition & 0 deletions rest-api-dynamodb/layers/orders/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# The AWS Lambda Python runtime provides boto3.
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,11 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
dynamodb_table_registry={"orders": table},
)

api = LambdaApi(self, "Api", lambda_path="lambdas", config=config)
api = LambdaApi(
self,
"Api",
lambda_path="lambdas",
layers_path="layers",
config=config,
)
CfnOutput(self, "ApiUrl", value=api.api.url)
Loading
Loading