Local development gateway for AWS SAM Lambda functions with FastAPI + Swagger UI
Project description
local-lambda-playground
Local development gateway for AWS SAM Lambda functions.
Reads yourtemplate.yml, discovers every HTTP route automatically, starts a FastAPI server with Swagger UI, assumes IAM roles, and hot-reloads on every file change — all from a single CLI command.
llp start --all
╭──────────────────────────────────────────────────────────────────────────────╮
│ Registered Routes │
├────────────┬────────┬───────────────────────────────┬────────────────────────┤
│ Group │ Method │ Path │ IAM Role │
├────────────┼────────┼───────────────────────────────┼────────────────────────┤
│ buyer-api │ POST │ /api/v1/auth/signup │ ambient │
│ buyer-api │ POST │ /api/v1/auth/login │ ambient │
│ buyer-api │ POST │ /api/v1/auth/refresh │ ambient │
│ buyer-api │ POST │ /api/v1/otp/send │ ambient │
│ seller-api │ POST │ /api/v1/seller/register │ agarmart-seller-role │
│ admin-api │ POST │ /api/v1/admin/auth/login │ agarmart-admin-role │
╰────────────┴────────┴───────────────────────────────┴────────────────────────╯
Swagger UI: http://localhost:8000/docs
ReDoc: http://localhost:8000/redoc
Table of Contents
- What Is local-lambda-playground?
- Why Use It?
- How It Compares to sam local start-api
- Requirements
- Installation
- Quick Start
- CLI Commands
- Filtering & Targeting Functions
- IAM Role Support
- Auth Mock
- Environment Variables & Configuration
- Hot Reload
- Swagger UI & API Documentation
- Dependency Isolation
- How It Works Internally
- Project Structure
- Testing
- Troubleshooting
What Is local-lambda-playground?
llp is a developer tool that makes it fast and painless to run AWS SAM Lambda functions on your local machine — without Docker, without SAM CLI, and without slow cold starts.
It reads your template.yml, discovers all AWS::Serverless::Function resources that have API Gateway events, and dynamically registers them as routes in a live FastAPI web server. Every route is immediately browsable in Swagger UI, hot-reloads when you change any source file, and runs with the exact same IAM permissions your function would have in production.
Why Use It?
The problem with sam local start-api
- Slow — spins up a Docker container for every request (3–10 seconds cold start)
- Opaque — no Swagger UI, no structured request/response logging
- Brittle — Docker dependency, port conflicts, environment variable setup is tedious
- No IAM — cannot assume real IAM roles locally
What local-lambda-playground gives you
| Benefit | Detail |
|---|---|
| ⚡ Instant startup | Python import, no Docker — functions start in milliseconds |
| 📖 Auto Swagger UI | Every route documented at /docs the moment the server starts |
| 🔄 Hot reload | Edit any handler .py or template.yml — server reloads automatically |
| 🔑 Real IAM roles | Calls STS AssumeRole using the Role: ARN from your template |
| 🧪 Auth mock | Bypass JWT auth in local testing with --mock-user-id |
| 🌿 Smart filtering | Run only buyer-api, or only Buyer* functions, or everything except Admin* |
| 📦 Zero extra config | Points at your existing template.yml — nothing else needed |
| 🗂 Grouped Swagger | Routes are tagged by folder group (buyer-api, seller-api, admin-api) |
| 🔒 Credential safety | IAM credentials injected per-invocation and cleaned up immediately after |
How It Compares to sam local start-api
| Capability | llp | sam local start-api |
|---|---|---|
| Startup speed | < 1 second | 5–30 seconds (Docker pull + container) |
| Requires Docker | No | Yes |
| Swagger UI | Yes — auto-generated | No |
| Route grouping in docs | Yes — by folder | No |
| Hot reload | Yes — uvicorn + watchdog | No (manual restart) |
| IAM role assumption | Yes — STS AssumeRole | No |
| Auth mock | Yes — --mock-user-id |
No |
| Per-function env vars | Yes — env.json per logical ID |
Yes — env.json |
| CF intrinsic handling | Graceful (strips/resolves) | Full CloudFormation engine |
| Parity with Lambda runtime | High (same Python interpreter) | Exact (Docker image match) |
| Offline usage | Yes | Yes (with container cached) |
Rule of thumb: Use
llpduring active development for fast feedback loops.
Usesam localfor final integration testing before deployment.
Requirements
- Python 3.10+
- AWS credentials configured (only needed for IAM role assumption — optional)
Installation
From PyPI
pip install local-lambda-playground
From source (development mode)
cd lambda-local/
pip install -e ".[dev]"
Verify
llp --help
Quick Start
# 1. Navigate to your SAM project
cd AgarMart-serverless-api/
# 2. Create a local env config (copy from example)
cp .env.example .env
# Fill in local values:
# MONGODB_URI=mongodb://localhost:27017
# REDIS_URL=redis://localhost:6379
# JWT_SECRET=my-dev-secret
# 3. Start ALL Lambda functions
llp start --all
# 4. Open Swagger UI
open http://localhost:8000/docs
That's it. Every route from your template.yml is live and documented.
CLI Commands
start — Launch the gateway
llp start [OPTIONS]
| Option | Short | Default | Description |
|---|---|---|---|
--all |
-a |
false |
Run ALL functions (ignores other filters) |
--function NAME |
-f |
— | Include only this function (repeatable) |
--group NAME |
-g |
— | Include functions in this folder group (repeatable) |
--match PATTERN |
-m |
— | Glob pattern for logical IDs e.g. Buyer* (repeatable) |
--exclude PATTERN |
-e |
— | Glob pattern to exclude e.g. Admin* (repeatable) |
--template PATH |
-t |
template.yml |
Path to SAM template |
--host HOST |
— | 127.0.0.1 |
Bind address |
--port PORT |
— | 8000 |
Bind port |
--reload / --no-reload |
— | --reload |
Enable / disable hot reload |
--env-file PATH |
— | auto | Path to .env file |
--env-json PATH |
— | auto | Path to env.json |
--mock-user-id ID |
— | — | Inject a fake user into requestContext.authorizer |
--mock-role ROLE |
— | buyer |
Role for the mock user |
--mock-email EMAIL |
— | dev@localhost |
Email for the mock user |
--role-arn ARN |
— | — | Assume this IAM role for ALL functions |
--no-role |
— | false |
Skip role assumption; use ambient AWS credentials |
list — Print all function IDs
llp list [--template PATH]
Lists every AWS::Serverless::Function logical ID in the template (alphabetically sorted).
AdminAuthFunction
AdminCategoriesFunction
BuyerAuthFunction
BuyerOtpFunction
SellerAuthFunction
SellerProductsFunction
...
routes — Print all HTTP routes
llp routes [--template PATH] [--group NAME]
Method Path Group Handler
POST /api/v1/auth/signup buyer-api auth/handler.lambda_handler
POST /api/v1/auth/login buyer-api auth/handler.lambda_handler
GET /api/v1/buyer/profile buyer-api profile/handler.lambda_handler
POST /api/v1/seller/register seller-api auth/handler.lambda_handler
GET /api/v1/seller/products seller-api products/handler.lambda_handler
POST /api/v1/admin/auth/login admin-api auth/handler.lambda_handler
Filter by group:
llp routes --group seller-api
invoke — Call a function directly
llp invoke FUNCTION_ID [OPTIONS]
| Option | Description |
|---|---|
--event PATH |
JSON file with the full Lambda event payload |
--template PATH |
Path to SAM template |
--env-json PATH |
Path to env.json |
--role-arn ARN |
Assume this IAM role before invoking |
--no-role |
Skip role assumption |
Basic invoke (event auto-built from route metadata):
llp invoke BuyerAuthFunction
Invoke with a custom event file:
llp invoke BuyerAuthFunction --event events/login.json
Example events/login.json:
{
"httpMethod": "POST",
"path": "/api/v1/auth/login",
"headers": { "Content-Type": "application/json" },
"body": "{\"email\": \"user@example.com\", \"password\": \"secret123\"}",
"queryStringParameters": null,
"pathParameters": null,
"requestContext": {
"stage": "local",
"identity": { "sourceIp": "127.0.0.1" },
"authorizer": {}
}
}
Output:
{
"statusCode": 200,
"headers": { "Content-Type": "application/json" },
"body": {
"success": true,
"data": {
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "Bearer"
}
}
}
install — Set up per-function venvs
llp install [OPTIONS]
| Option | Description |
|---|---|
--group NAME |
Install only this group (repeatable) |
--force |
Recreate venvs from scratch |
--template PATH |
Path to SAM template |
Creates an isolated virtual environment for each function group:
.lambda-local/
├── buyer-api/ ← buyer-api/requirements.txt installed here
├── seller-api/
└── admin-api/
llp install # all groups
llp install -g buyer-api
llp install --force # rebuild from scratch
Add
.lambda-local/to your.gitignore.
Filtering & Targeting Functions
Run everything
llp start --all
Single function
llp start -f BuyerAuthFunction
Multiple specific functions
llp start -f BuyerAuthFunction -f BuyerOtpFunction
By folder group
The group comes from the first segment of CodeUri:
BuyerAuthFunction:
Properties:
CodeUri: buyer-api/auth/ # group = "buyer-api"
llp start --group buyer-api
llp start --group buyer-api --group seller-api
Wildcard pattern
llp start --match "Buyer*" # everything starting with Buyer
llp start --match "*Auth*" # everything containing Auth
llp start --match "Admin*" # all admin functions
Exclude
llp start --all --exclude "Admin*"
llp start --all --exclude "*Job*"
Combine
# buyer-api but not the social auth endpoint
llp start --group buyer-api --exclude "BuyerSocialAuthFunction"
IAM Role Support
In production, each Lambda function runs under an IAM execution role:
BuyerAuthFunction:
Type: AWS::Serverless::Function
Properties:
Role: arn:aws:iam::123456789012:role/agarmart-buyer-role
# or with CloudFormation Sub:
Role: !Sub 'arn:aws:iam::${AWS::AccountId}:role/agarmart-${StageName}'
llp replicates this by calling STS AssumeRole before each function invocation and injecting the temporary credentials as environment variables — so your function can call S3, SQS, DynamoDB, or any AWS service with real permissions.
How credentials are resolved
Request arrives
│
▼
Read role_arn from FunctionRoute
(extracted from template.yml Role: property)
│
▼
--role-arn override? → use that instead
│
▼
Resolve CloudFormation Sub variables:
${AWS::AccountId} → real account ID via STS GetCallerIdentity
${AWS::Region} → boto3 default region
${AWS::Partition} → aws
│
▼
Check credential cache (cached for 1 hour)
Cache hit → use cached credentials
Cache miss → STS AssumeRole(...)
│
▼
Inject into environment:
AWS_ACCESS_KEY_ID = <temp>
AWS_SECRET_ACCESS_KEY = <temp>
AWS_SESSION_TOKEN = <temp>
│
▼
Call lambda_handler(event, context)
│
▼
Restore original credentials
CLI options
# Per-function roles from template.yml (default behaviour)
llp start --all
# Override: assume one role for ALL functions
llp start --all \
--role-arn arn:aws:iam::123456789012:role/my-dev-role
# Skip role assumption entirely
llp start --all --no-role
# Set via environment variable (persists across reloads)
export LAMBDA_LOCAL_ROLE_ARN=arn:aws:iam::123456789012:role/my-dev-role
llp start --all
# invoke command also supports role options
llp invoke BuyerAuthFunction \
--role-arn arn:aws:iam::123456789012:role/agarmart-dev
CloudFormation Sub variables resolved
| Variable | Resolved to |
|---|---|
${AWS::AccountId} |
Real account ID via sts:GetCallerIdentity |
${AWS::Region} |
boto3 default region or AWS_REGION env var |
${AWS::Partition} |
aws |
${StageName} (custom) |
Stripped (empty string — override with --role-arn) |
Required developer permissions
To assume a role, your IAM user/role needs:
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::123456789012:role/agarmart-*"
}
And the target role must list your IAM principal in its Trust Policy.
Graceful fallback
If AssumeRole fails (no permission, unresolvable ARN, no AWS credentials), llp logs a warning and continues with ambient credentials. The server never crashes on a role assumption failure. Use --no-role to suppress the warning.
Auth Mock
Most Lambda functions check event["requestContext"]["authorizer"] for user identity (injected by the Lambda Authorizer in production). In local dev you don't want to generate real JWTs.
Use --mock-user-id to inject a fake user into every request:
llp start --all \
--mock-user-id user_abc123 \
--mock-role buyer \
--mock-email test@example.com
Every request will contain:
{
"requestContext": {
"authorizer": {
"user_id": "user_abc123",
"role": "buyer",
"email": "test@example.com"
}
}
}
Your handler's get_current_user(event) succeeds without any JWT validation.
Test different roles simultaneously:
# Terminal 1 — buyer on port 8001
llp start --group buyer-api --port 8001 \
--mock-user-id buyer_001 --mock-role buyer
# Terminal 2 — admin on port 8002
llp start --group admin-api --port 8002 \
--mock-user-id admin_001 --mock-role admin
Environment Variables & Configuration
Auto-detection
llp scans the template's directory automatically:
AgarMart-serverless-api/
├── template.yml
├── .env.local ← checked first
├── .env ← checked second
└── env.json ← checked third
Explicit paths
llp start --env-file .env.staging --env-json env.staging.json
.env format
MONGODB_URI=mongodb://localhost:27017
MONGODB_DB_NAME=agarmart
REDIS_URL=redis://localhost:6379
JWT_SECRET=dev-only-secret-256-bits
JWT_ACCESS_EXPIRE_MINUTES=15
JWT_REFRESH_EXPIRE_DAYS=30
AWS_S3_BUCKET=agarmart-dev-media
AWS_REGION=ap-south-1
CDN_BASE_URL=https://cdn.innobuz.com/app
BREVO_API_KEY=xkeysib-...
FAST2SMS_API_KEY=...
GOOGLE_CLIENT_ID=...
SQS_EMAIL_QUEUE_URL=https://sqs.ap-south-1.amazonaws.com/123456789012/agarmart-email-dev
SQS_SMS_QUEUE_URL=https://sqs.ap-south-1.amazonaws.com/123456789012/agarmart-sms-dev
SQS_PUSH_QUEUE_URL=https://sqs.ap-south-1.amazonaws.com/123456789012/agarmart-push-dev
env.json — SAM Parameters format
{
"Parameters": {
"MONGODB_URI": "mongodb://localhost:27017",
"JWT_SECRET": "dev-secret",
"REDIS_URL": "redis://localhost:6379"
}
}
env.json — Per-function format
Different values per Lambda function:
{
"BuyerAuthFunction": {
"MONGODB_URI": "mongodb://localhost:27017/buyer_test"
},
"AdminAuthFunction": {
"MONGODB_URI": "mongodb://localhost:27017/admin_test",
"JWT_SECRET": "admin-specific-secret"
}
}
CloudFormation intrinsic handling
template.yml often contains SSM references that can't be resolved locally:
JWT_SECRET: !Sub '{{resolve:ssm:/agarmart/prod/jwt_secret}}'
llp strips these automatically and replaces them with empty strings — the server starts without errors. Override with real local values in your .env.
Priority order (highest wins)
1. Actual process environment (export MY_VAR=x before starting)
2. --env-file / .env.local / .env
3. --env-json / env.json (Parameters key)
4. Per-function env.json block
Hot Reload
Hot reload is on by default. The server watches:
| File change | Effect |
|---|---|
Any **/*.py handler file |
Module reloaded; next request uses new code |
template.yml |
Routes re-discovered; Swagger UI updates |
env.json / .env |
Environment re-loaded |
requirements.txt |
No auto-install; run llp install then restart |
# Reload is on by default
llp start --all
# Disable (useful in CI)
llp start --all --no-reload
Swagger UI & API Documentation
| URL | Interface |
|---|---|
http://localhost:8000/docs |
Swagger UI — interactive API explorer |
http://localhost:8000/redoc |
ReDoc — clean reference docs |
http://localhost:8000/openapi.json |
Raw OpenAPI 3.0 schema |
Routes are tagged by group (folder prefix) so Swagger UI shows:
► buyer-api
POST /api/v1/auth/signup
POST /api/v1/auth/login
GET /api/v1/buyer/profile
► seller-api
POST /api/v1/seller/register
GET /api/v1/seller/products
► admin-api
POST /api/v1/admin/auth/login
GET /api/v1/admin/dashboard/stats
If you started with --mock-user-id, protected routes work directly in the Swagger UI without needing a Bearer token.
How It Works Internally
llp start --all
│
▼
┌─────────────────────────────────────────────────────────┐
│ CLI (cli.py) │
│ Parse args → preview table → set LAMBDA_LOCAL_* env │
│ → start uvicorn with factory=True reload=True │
└────────────────────┬────────────────────────────────────┘
│ on every (re)start
▼
┌─────────────────────────────────────────────────────────┐
│ Gateway (gateway.py) create_app() │
│ Read LAMBDA_LOCAL_* env vars │
│ → discover_routes() │
│ → detect Lambda layer paths (shared/python/) │
│ → create FastAPI app with Swagger tags │
│ → register one route per HTTP event │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Discovery (discovery.py) │
│ CF-aware YAML loader (handles !Ref, !Sub, !GetAtt) │
│ Extract: logical_id handler code_uri path method │
│ group role_arn │
│ Apply --function / --group / --match / --exclude │
└─────────────────────────────────────────────────────────┘
HTTP request arrives
│
▼
┌─────────────────────────────────────────────────────────┐
│ Route handler closure (gateway.py) │
│ → Invoker.invoke(route, request) │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Invoker (invoker.py) │
│ Build API Gateway event dict from FastAPI request │
│ Submit to ThreadPoolExecutor │
│ (asyncio.run() inside handlers is safe in threads) │
└────────────────────┬────────────────────────────────────┘
│ in thread
▼
┌─────────────────────────────────────────────────────────┐
│ _invoke_in_thread │
│ │
│ 1. Inject per-function env vars (env.json) │
│ │
│ 2. IAM credential injection (iam.py) │
│ • Resolve ${AWS::AccountId} etc. in role ARN │
│ • STS AssumeRole (cached 1 hr, refresh 5 min early) │
│ • Set AWS_ACCESS_KEY_ID / SECRET / SESSION_TOKEN │
│ │
│ 3. sys.path: prepend code_uri + layer paths │
│ │
│ 4. importlib.util.spec_from_file_location │
│ unique module key per logical_id │
│ (no naming conflicts across handlers) │
│ │
│ 5. handler_fn(event, context) │
│ │
│ 6. Restore IAM credentials + env vars │
└─────────────────────────────────────────────────────────┘
Project Structure
lambda-local/
├── pyproject.toml ← packaging, deps, tool config (Ruff, MyPy, pytest)
├── README.md
│
├── lambda_local/
│ ├── __init__.py
│ ├── cli.py ← Typer CLI (start, list, routes, invoke, install)
│ ├── config.py ← .env / env.json loading + CF intrinsic stripping
│ ├── discovery.py ← SAM template parser + FunctionRoute builder
│ ├── gateway.py ← FastAPI app factory (create_app)
│ ├── iam.py ← STS AssumeRole + credential caching + injection
│ ├── installer.py ← Per-group venv creator (llp install)
│ ├── invoker.py ← Lambda handler runner (thread pool executor)
│ ├── models.py ← Pydantic models: FunctionRoute, InvocationResult
│ └── watcher.py ← watchdog FileWatcher (debounced 500ms)
│
└── tests/
├── conftest.py ← Fixtures: templates, handler files
├── test_discovery.py ← Route discovery + filtering
├── test_invoker.py ← Event building + handler invocation
├── test_gateway.py ← FastAPI app + HTTP integration tests
└── test_cli.py ← CLI command tests (Typer TestClient)
Testing
pip install -e ".[dev]"
# Full suite with coverage
pytest
# Coverage HTML report
pytest --cov=lambda_local --cov-report=html
open htmlcov/index.html
# Specific file
pytest tests/test_discovery.py -v
# Single test
pytest tests/test_invoker.py::TestBuildEvent::test_mock_user_injected -v
Target coverage: > 90% (enforced by pytest config).
Troubleshooting
requires a different Python: 3.x not in '>=3.10'
Switch to Python 3.10+:
pyenv install 3.12.0 && pyenv local 3.12.0
pip install local-lambda-playground
Template not found: template.yml
Run from the directory containing your template, or pass --template:
llp start --all --template /path/to/AgarMart-serverless-api/template.yml
Handler file not found
Check that CodeUri is correct relative to template.yml and that Handler matches the actual filename:
CodeUri: buyer-api/auth/ # directory must exist
Handler: handler.lambda_handler # buyer-api/auth/handler.py must exist
ImportError: No module named 'agarmart'
The shared layer isn't on the Python path. Declare it in template.yml:
SharedLayer:
Type: AWS::Serverless::LayerVersion
Properties:
ContentUri: shared/ # shared/python/ is added to sys.path automatically
Or install it directly:
pip install -r shared/requirements.txt
ModuleNotFoundError: No module named 'motor'
Install the function's runtime deps:
pip install -r buyer-api/requirements.txt
pip install -r shared/requirements.txt
Or use the isolation approach:
llp install --group buyer-api
iam_assume_role_failed warning
Causes and fixes:
| Cause | Fix |
|---|---|
| No AWS credentials | aws configure or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY |
No sts:AssumeRole permission |
Add the permission to your IAM user/role |
Unresolvable ARN (${StageName} etc.) |
Pass --role-arn arn:aws:iam::ACCOUNT:role/NAME explicitly |
To suppress the warning and use ambient credentials:
llp start --all --no-role
Routes not updating after template edit
- Confirm
--reloadis active (it is by default) - Check you're not in the
.lambda-local/directory (excluded from watching) - Save the file again — some editors write atomically without triggering inotify
- Hard restart:
Ctrl+C→llp start --all
License
MIT
[pypi] username = innux password = pypi-AgEIcHlwaS5vcmcCJDc5MjE1YTQ5LTEwNWItNDdhMS05MTI4LWFjNzY3MWVlYTFjMAACKlszLCJkMTI3ZGYyNS0wYjZhLTRiNjItODE4Yi1lODI5ODczOGRkMDUiXQAABiApc8NZEWfv0eaHXv1t1DeZv1Yix46pSmTkLPMq9a1CMA
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file local_lambda_playground-0.1.1.tar.gz.
File metadata
- Download URL: local_lambda_playground-0.1.1.tar.gz
- Upload date:
- Size: 42.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.17
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7184d779957ac49ebdc8b3161c815cb07fe0f161421846010d1753692db28a07
|
|
| MD5 |
15221a3eef50621e622dfe741fcd8b64
|
|
| BLAKE2b-256 |
567767720fbbba7b64691c1a6d9abb68ca171b88fff48ccadc8255499985779f
|
File details
Details for the file local_lambda_playground-0.1.1-py3-none-any.whl.
File metadata
- Download URL: local_lambda_playground-0.1.1-py3-none-any.whl
- Upload date:
- Size: 35.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.17
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6d3f5ba624f0244e93d9df816697a6916ba434b5246832ab2c7297055cf56ec3
|
|
| MD5 |
a6efeb8f0eb33e79720bf5918cf9473d
|
|
| BLAKE2b-256 |
0b52cd96d97078522aab69d78fe199a253e16f22a505bfcef983d4c9d4629773
|