LambdaApi — Lambdas behind API Gateway¶
sosw.lambda_api.LambdaApi is a Processor subclass for AWS Lambda functions
that serve HTTP requests behind Amazon API Gateway. One Lambda backs a whole (micro)API: you
declare a route table in the config, implement the handlers as methods, and LambdaApi takes
care of event parsing, authorization, routing, response rendering and the error contract.
Both API Gateway payload formats are supported: REST API (v1.0) and HTTP API (v2.0) proxy events.
Minimal example¶
import json
from sosw.app import LambdaGlobals, get_lambda_handler
from sosw.components.exceptions import NotFoundError
from sosw.lambda_api import LambdaApi
class Processor(LambdaApi):
DEFAULT_CONFIG = {
'auth_enabled': True,
'routes': {
'GET /things': 'list_things',
'GET /things/{thing_id}': 'get_thing',
'POST /things': 'create_thing',
},
'things_dynamo_db_config': {
'table_name': 'things',
'row_mapper': {'thing_id': 'S'},
},
}
def list_things(self, event, **kwargs):
return {'things': self.get_ddbc('things').get_by_scan()}
def get_thing(self, event, thing_id=None, **kwargs):
things = self.get_ddbc('things').get_by_query({'thing_id': thing_id})
if not things:
raise NotFoundError(f"Thing {thing_id} does not exist")
return {'thing': things[0]}
def create_thing(self, event, **kwargs):
thing = json.loads(event.get('body') or '{}')
self.get_ddbc('things').put(thing)
return self.make_response({'created': thing['thing_id']}, status_code=201)
global_vars = LambdaGlobals()
lambda_handler = get_lambda_handler(Processor, global_vars)
The regular sosw.app.get_lambda_handler() is used — LambdaApi inherits the full
warm start behavior, and the compiled router is cached on the instance for
the lifetime of the container.
Routing¶
The routes config parameter maps 'METHOD /path' keys to names of handler methods:
Paths use the API Gateway resource syntax:
{param}matches one path segment, the greedy{proxy+}matches the rest of the path including slashes.Handlers receive the raw event as the first positional argument and the extracted path parameters (URL-decoded) as keyword arguments:
def get_thing(self, event, thing_id=None, **kwargs).Routes without placeholders are matched before parametrized ones, so
GET /things/countwins overGET /things/{thing_id}regardless of declaration order.Request paths are normalized before matching: the query string and the trailing slash are stripped (
/things/and/things?page=2both match/things).If the deployed API serves routes under a stage or mount prefix, list it in
path_prefixes(e.g.'path_prefixes': ['/prod']turns/prod/thingsinto/things).
A handler may return:
any JSON-serializable object — rendered as a
200response;a pre-rendered dict containing
statusCode(typically built withmake_response(body, status_code=..., headers=...)) — passed through to API Gateway untouched;or raise an
ApiErrorsubclass — rendered as the corresponding HTTP error envelope.
Responses, CORS and JSON encoding¶
make_response(body, status_code=200, headers=None) renders the API Gateway proxy response
{'statusCode', 'headers', 'body'}. Headers are the config-driven cors_headers (the
defaults allow every origin — tighten them for production) merged with the per-call headers.
The JSON encoder handles the types DynamoDB-backed code returns without ceremony:
Decimal→intwhen integral, elsefloat;datetime/date→ ISO-8601 string;set/frozenset→ sorted list.
Override the static method json_default to support more types (e.g. UUID).
The error contract¶
Handlers (and the pipeline itself) communicate failures by raising the
ApiError hierarchy from
sosw.components.exceptions:
Exception |
HTTP status |
|
Raised by the pipeline when |
|---|---|---|---|
|
400 |
|
the event is not an API Gateway proxy event |
|
401 |
|
claims are missing or the token expired |
|
403 |
|
the caller is in none of |
|
404 |
|
no configured route matches method + path |
|
409 |
|
(your handlers) |
|
500 |
|
any unexpected exception |
Every error renders the same machine-readable envelope:
{
"error": {
"code": "NOT_FOUND",
"message": "No route configured for: GET /nope"
}
}
A custom message travels in message; raise ConflictError(f"Thing {thing_id} already
exists") produces HTTP 409 with the envelope carrying your text.
The exact semantics of the pipeline:
Authorization runs before routing. A request with missing/invalid claims receives
401even for a path that does not exist — unauthenticated callers cannot probe the route table.Unknown route or method → 404. There is no automatic
405: a known path with an unconfigured method is just as much “not found”.Only ApiError maps to specific statuses. Any other exception escaping a handler is logged server-side with the full traceback, counted in stats, and rendered as the generic
500envelope — internals never leak to the client.
Note
Unlike regular sosw Processors, which must fail fast and loud, LambdaApi.__call__
intentionally converts exceptions into well-formed HTTP responses: an API Gateway integration
must always answer the client. This is the API contract layer, not silent error handling —
everything is logged and counted server-side.
REST API (v1) vs HTTP API (v2)¶
LambdaApi absorbs the differences between the two payload formats; for reference:
Aspect |
REST API (payload v1.0) |
HTTP API (payload v2.0) |
|---|---|---|
Method / path |
|
|
Claims |
|
|
|
numeric |
numeric string |
|
list |
stringified |
Fallback |
— |
|
Statistics¶
LambdaApi counts per-container statistics through the standard self.stats mechanism:
api_calls— every invocation;api_route_get_things_thing_id— per matched route (snake_case of the route key, e.g.'GET /things/{thing_id}');api_errors_4xx/api_errors_5xx— per error family.
Deployment¶
See the HTTP API with LambdaApi tutorial for a complete SAM template
with a Cognito JWT authorizer and curl-level verification of the whole contract.
API reference¶
View Licence Agreement- class sosw.lambda_api.LambdaApi(*args, **kwargs)[source]¶
Base Processor for AWS Lambda functions behind API Gateway. Supports both the REST API (payload version 1.0) and the HTTP API (payload version 2.0) proxy event shapes.
Provides a declarative router configured with the
routesconfig parameter, Cognito JWT claims extraction and validation, config-driven CORS headers, JSON response rendering with a DynamoDB-friendly encoder, and a uniform machine-readable error envelope.Subclasses implement the handler methods and declare them in
routes:from sosw.app import LambdaGlobals, get_lambda_handler from sosw.lambda_api import LambdaApi class Processor(LambdaApi): DEFAULT_CONFIG = { 'auth_enabled': True, 'routes': { 'GET /things': 'list_things', 'GET /things/{thing_id}': 'get_thing', }, } def list_things(self, event, **kwargs): return {'things': self.get_ddbc('things').scan()} def get_thing(self, event, thing_id=None, **kwargs): return {'thing': self.get_ddbc('things').get_by_query({'thing_id': thing_id})} global_vars = LambdaGlobals() lambda_handler = get_lambda_handler(Processor, global_vars)
Handlers receive the raw event as the first positional argument and the path parameters extracted from the request path (per the
{param}placeholders of the matched route) as keyword arguments. A handler may return:any JSON-serializable object - rendered as a
200response body;a pre-rendered response dict containing
statusCode(e.g. built withmake_response()using a custom status code or extra headers) - passed through to API Gateway untouched;or raise
sosw.components.exceptions.ApiError(or a subclass) - rendered as the corresponding HTTP error envelope.
Note
Unlike regular
soswProcessors which are required to fail fast and loud,__call__of this class intentionally converts exceptions to well-formed HTTP error responses: an API Gateway integration must always return a valid response to the client. This is the API contract layer, not silent error handling: every exception is logged server-side (unexpected ones with the full traceback), counted inself.stats, and unexpected exceptions are never exposed to the client beyond a generic500envelope.Configuration parameters of this class:
routes- dict mapping'METHOD /path'route keys to names of handler methods. Paths support API Gateway resource syntax placeholders:{param}and greedy{proxy+}.auth_enabled- whenTrue(secure default), requests must carry Cognito claims injected by the API Gateway authorizer, otherwise401is returned.authorized_groups- optional list of Cognito group names. When non-empty, the caller must be a member of at least one of them (thecognito:groupsclaim), otherwise403.cors_headers- dict of headers attached to every rendered response.path_prefixes- optional list of prefixes (e.g. stage or mount prefixes) stripped from the request path before route matching.
- parse_event(event: dict) tuple[source]¶
Extract the HTTP method and the normalized request path from an API Gateway proxy event.
Supported event shapes:
REST API v1:
httpMethod+path;HTTP API v2:
requestContext.http.method+rawPath;trimmed v2 events carrying only
routeKey('GET /things') as the last resort.
The path is normalized (query string and trailing slash removed) and the first matching entry of the
path_prefixesconfig parameter is stripped from it.- Parameters:
event (dict) – Raw Lambda event.
- Return type:
tuple
- Returns:
Tuple of (method, path), method upper-cased.
- Raises:
BadRequestError – If the event does not look like an API Gateway proxy event.
- get_router() list[source]¶
Return the compiled router, building it on the first request of the container.
The router is built from the
routesconfig parameter bybuild_router()and cached on the instance for the container lifetime, so warm invocations skip parsing and regex compilation entirely.- Return type:
list
- Returns:
List of compiled route entries.
- build_router() list[source]¶
Compile the
routesconfig parameter into a list of route entries.Each
'METHOD /path': 'handler_name'config pair becomes a dict with the original route key, the upper-cased method, the normalized path template, the compiled path pattern and the bound handler method. Routes without path parameters are ordered before parametrized ones, soGET /things/countwins overGET /things/{thing_id}regardless of the declaration order. Within each group the config declaration order is preserved.- Return type:
list
- Returns:
List of compiled route entries.
- Raises:
ValueError – If a route key is malformed or a handler method is missing.
- static compile_path_pattern(path_template: str) Pattern[source]¶
Compile a path template with API Gateway resource syntax into a regex pattern.
{param}placeholders match a single path segment as the named groupparam. The greedy{proxy+}form matches the rest of the path (including slashes) asproxy.- Parameters:
path_template (str) – Path template, e.g.
'/things/{thing_id}'.- Return type:
re.Pattern
- Returns:
Compiled pattern matching the whole request path.
- match_route(method: str, path: str) tuple[source]¶
Find the route entry matching the request and extract its path parameters.
Path parameter values are URL-decoded. Requests that match no configured route (either by path or by method) raise
sosw.components.exceptions.NotFoundError->404.- Parameters:
method (str) – Upper-cased HTTP method of the request.
path (str) – Normalized request path.
- Return type:
tuple
- Returns:
Tuple of (route entry, dict of extracted path parameters).
- Raises:
NotFoundError – When no configured route matches the request.
- static get_claims(event: dict) dict[source]¶
Extract Cognito JWT claims from either API Gateway authorizer event shape.
REST API v1 (Cognito User Pool authorizer):
requestContext.authorizer.claimsHTTP API v2 (JWT authorizer):
requestContext.authorizer.jwt.claims
Both the id and access token claim sets are supported - the method does not require any specific claim to be present.
- Parameters:
event (dict) – API Gateway proxy event.
- Return type:
dict
- Returns:
The claims dictionary, or an empty dict if the event carries none.
- validate_authorization(event: dict) dict[source]¶
Validate the caller identity from the claims injected by the API Gateway authorizer.
The authorizer has already verified the JWT signature; this method enforces that claims are present, the token is not expired, and (when the
authorized_groupsconfig parameter is non-empty) the caller belongs to at least one authorized Cognito group.Error responses never include claim contents; the reasons are logged server-side only.
- Parameters:
event (dict) – API Gateway proxy event.
- Return type:
dict
- Returns:
The validated claims.
- Raises:
UnauthorizedError – When claims are missing or the token is expired ->
401.ForbiddenError – When the caller is not in any authorized group ->
403.
- static token_expired(exp: Any) bool[source]¶
Check whether the
expclaim points to the past.Tolerated formats: numeric epoch (the standard JWT claim), numeric strings (the form HTTP API v2 authorizers deliver), and the legacy custom-authorizer string format
'Wed Aug 06 09:02:12 UTC 2025'. A missing or unparsable claim is not treated as expired - the API Gateway authorizer remains the authority on token validity.- Parameters:
exp – Value of the
expclaim in any supported format.- Return type:
bool
- static parse_groups(raw_groups: Any) list[source]¶
Parse the
cognito:groupsclaim into a list of group names.Tolerated formats: a real list (REST API v1), the stringified
'[admins users]'form delivered by HTTP API v2 JWT authorizers, and comma- or space-separated plain strings.- Parameters:
raw_groups – Raw value of the
cognito:groupsclaim.- Return type:
list
- check_route_access(route: str, claims: dict) None[source]¶
Per-route access control hook, called after route matching and before the handler.
The base implementation allows everything. Override in subclasses to implement custom RBAC (e.g. permission tables in DynamoDB, interface grants, organization scopes) and raise
sosw.components.exceptions.ForbiddenError(->403) orsosw.components.exceptions.UnauthorizedError(->401) to deny the request.- Parameters:
route (str) – The matched route key exactly as declared in the
routesconfig, e.g.'GET /things/{thing_id}'.claims (dict) – Validated claims of the caller (empty dict when
auth_enabledis off and the event carries no claims).
- make_response(body: Any = None, status_code: int = 200, headers: dict = None) dict[source]¶
Render an API Gateway proxy response with the configured CORS headers.
The body is JSON-serialized with
json_default(), which supports the types DynamoDB and datetime-heavy code commonly return (Decimal,datetime,date,set).- Parameters:
body – JSON-serializable payload, or
Nonefor an empty body.status_code (int) – HTTP status code of the response.
headers (dict) – Extra headers merged over the configured
cors_headers.
- Return type:
dict
- Returns:
API Gateway proxy response:
{'statusCode', 'headers', 'body'}.
- make_error_response(error: ApiError) dict[source]¶
Render the uniform error envelope for an ApiError and count the error statistics.
The envelope is
{'error': {'code': <MACHINE_CODE>, 'message': <human readable>}}. Statistics:api_errors_4xx/api_errors_5xxper status code family.- Parameters:
error (ApiError) – The error to render.
- Return type:
dict
- Returns:
API Gateway proxy response with the error envelope.
- static json_default(value: Any) Any[source]¶
JSON encoder fallback for types that
json.dumpsdoes not handle natively.Decimal(as returned by DynamoDB) -intwhen integral, elsefloat;datetime/date- ISO-8601 string;set/frozenset- sorted list (plain list when elements are not sortable).
Override in subclasses to support more types (e.g.
UUID).- Parameters:
value – Value that JSON could not serialize.
- Returns:
A JSON-serializable representation.
- Raises:
TypeError – For unsupported types (standard
jsoncontract).
- strip_path_prefix(path: str) str[source]¶
Strip the first matching entry of the
path_prefixesconfig parameter from the path.Useful when the deployed API serves routes under a stage or mount prefix (e.g.
/prod/things->/thingswith'path_prefixes': ['/prod']).- Parameters:
path (str) – Normalized request path.
- Return type:
str