Local Serverless Without a Routes File: Hot Reload, Custom Routes, and Live OpenAPI¶
Verified status as of March 28, 2026. Runtime note: FastFN auto-installs function-local dependencies from
requirements.txt/package.json; host runtimes are required infastfn dev --native, whilefastfn devdepends on a running Docker daemon.
Why this matters¶
A lot of local serverless setups fail in one of two ways: - they require a central routing file nobody wants to maintain, - or docs and runtime drift apart.
fastfn avoids both by discovering functions from the filesystem and generating OpenAPI from each function's config.
Quick docs map¶
- First boot and tests: Run & Test
- Manage files from console/API: Manage Functions
- Function shape and options: Function Spec
- Available endpoints: HTTP API
- Runtime payload contract: Runtime Contract
- Why invocation works this way: Invocation Flow
Core idea¶
You define a function by files under:
- functions/<name>/
Optionally versioned:
- functions/<name>/<version>/
No global routes.json is required.
The gateway discovers functions automatically and applies policy from fn.config.json.
Step 1: Create a tiny function¶
Create functions/my-profile/handler.py:
import json
def handler(event):
query = (event or {}).get("query") or {}
name = query.get("name", "world")
return {
"status": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"hello": name})
}
Create functions/my-profile/fn.config.json:
{
"timeout_ms": 1200,
"max_concurrency": 10,
"invoke": {
"methods": ["GET"],
"summary": "Simple profile demo",
"query": {"name": "World"}
}
}
Step 2: Reload catalog immediately¶
You now avoid restart loops while iterating.
Step 3: Invoke the function¶
Step 4: Verify OpenAPI is live¶
Confirm:
- /my-profile exists,
- method list matches invoke.methods.
If you change methods to POST and reload, OpenAPI updates.
Step 5: Add a real custom endpoint¶
Edit fn.config.json:
{
"invoke": {
"methods": ["GET"],
"routes": ["/api/profile", "/public/whoami"],
"summary": "Profile via custom routes"
}
}
Reload and test:
curl -sS 'http://127.0.0.1:8080/api/profile?name=Misael'
curl -sS 'http://127.0.0.1:8080/public/whoami?name=Misael'
Step 6: Add versioning with zero confusion¶
Create a second version:
- functions/my-profile/v2/handler.py
- functions/my-profile/v2/fn.config.json
Invoke both:
Step 7: Use custom handler names (Lambda-style)¶
If you do not want the function symbol to be named handler, use invoke.handler.
Keep the distinction clear:
- file selection comes from
entrypoint, file routes, or canonical entry files such ashandler.*/main.*/index.* - symbol selection comes from
invoke.handler - Python also accepts
main(req)whenhandleris absent
fn.config.json:
Then export/define main in your runtime file.
Common pitfalls and fixes¶
| Problem | Cause | Fix |
|---|---|---|
| function not found | wrong folder naming | match the functions/<name>[/<version>] layout or your file-route tree exactly |
| OpenAPI doesn't change | catalog not reloaded | call POST /_fn/reload |
| method mismatch | invoke.methods changed but test still old |
verify method in both curl and OpenAPI |
| route conflict | same custom route in two functions | rename one route; conflicts return 409 |
| ambiguous function | same name in multiple runtimes | use explicit runtime path in console APIs or avoid duplicate names |
Why teams like this model¶
- policy close to code,
- no giant central config,
- doc/runtime parity through OpenAPI generation,
- fast local inner loop.
Related docs¶
Key takeaway¶
The fastest loop here is simple: add a file, reload once, call the route, check OpenAPI. When those four steps stay aligned, local development feels predictable instead of magical.
What to keep in mind¶
- File layout controls discovery, while
fn.config.jsoncontrols methods, routes, and handler overrides. - Reload after config or route changes so the catalog and OpenAPI stay in sync.
- Version folders are a clean way to ship a new contract without breaking the old one.
When to choose a different setup¶
- Use explicit routes when you must preserve an existing public path exactly.
- Use a long-lived service process only if your feature depends on shared in-memory state across many routes.
- Keep this model for function-sized HTTP work where fast iteration and contract visibility matter most.