build-fullstack-backend
GitHub提供后端及全栈应用生成的强制规范,涵盖FastAPI+Mangum模板、路由前缀、静态文件布局及AWS Lambda部署契约,确保代码可正确启动与发布。
触发场景
安装
npx skills add mindsdb/anton --skill build-fullstack-backend -g -y
SKILL.md
Frontmatter
{
"name": "build-fullstack-backend",
"metadata": {
"provenance": "builtin",
"display_name": "Backend & fullstack app generation"
},
"description": "MANDATORY reading before writing ANY backend, API, server, or fullstack application code (create_artifact types fullstack-stateless-app \/ fullstack-stateful-app, or anything that will be launched with launch_backend). Contains the complete hard contract: the canonical FastAPI+Mangum backend.py template, SECRETS handling, \/api\/* route prefix rules, static\/ frontend layout, requirements.txt, and the launch\/preview workflow. Building a backend without recalling this skill first WILL break launch and deployment. When in doubt, recall it."
}
BACKEND & FULLSTACK APPLICATION GENERATION:
When the user asks to build a backend service, web application with a backend, or API-driven system, follow this workflow. It covers BOTH fullstack artifact types — the steps are identical; only the LOCAL STATE rule (see RULES) differs.
HARD CONTRACT (violating ANY of these breaks launch or deployment — full explanations in the RULES of step 4):
- The backend file is
<artifact_path>/backend.py; thehandlerattribute and theSECRETSdict keep exactly those names. handler = Mangum(app, lifespan="off").- ALL API routes live under
/api/*and are registered BEFOREapp.mount("/", StaticFiles(...)). - The script accepts
--portvia argparse and binds to it — never hardcode a port. - The entire frontend lives in
<artifact_path>/static/, entry-pointstatic/index.html. <artifact_path>/requirements.txtexists and lists at leastfastapi,mangum,uvicorn.- Secrets are read from
SECRETS[...]at their point of use inside routes — never copied into module-level variables at import time.
- REGISTER THE ARTIFACT: Follow the universal artifact contract from the ARTIFACTS section. For backend apps specifically:
type: pick between the two fullstack types:"fullstack-stateless-app"— the DEFAULT. Always start here. The app keeps NO local state between requests (the deployment target is stateless: AWS Lambda with a read-only filesystem, see RULES and DEPLOYMENT NOTES below); all persistence goes through external data sources."fullstack-stateful-app"— ONLY when the app genuinely requires local on-disk state between requests (e.g. a SQLite DB) AND that state cannot live in an external connected data source. When in doubt, choose stateless.
primary: set to"static/index.html"— the frontend ALWAYS lives in astatic/subfolder of the artifact (see steps 4 and 5 below). Use the returned<artifact_path>for ALL subsequent writes —backend.pyandrequirements.txtgo directly in<artifact_path>/; ALL frontend files (HTML, CSS, JS, images, fonts) go into<artifact_path>/static/.
- TECHNICAL SPECIFICATION (as a system analyst): Create a brief technical specification for the application. The specification MUST include:
- Brief description of what the application does (keep it concise)
- Core features and requirements
- REST API specification in markdown format with:
- Endpoints and HTTP methods
- Request/response schemas (JSON examples)
- Error handling
- Framework: ALWAYS use FastAPI. No other framework is supported here — every backend MUST be FastAPI so it can be invoked both locally and as an AWS Lambda function via the canonical template in step 4.
- Key dependencies and libraries needed (in addition to the mandatory
fastapi,mangum,uvicorn— see step 4)
- FETCH & VALIDATE SAMPLE DATA: Using the scratchpad tool:
- Fetch representative sample data from the user's data source (API, database, file)
- Get enough data to understand: structure, data types, volume, and shape
- Answer these questions:
- Is the fetched data sufficient for building the application per the spec?
- Can this data type be used to implement the API as designed?
- Do we need different/more data, or should the spec be revised?
- If the answer to any question is "no" — go back to step 2 and revise the technical specification based on what you learned about the actual data
- IMPLEMENT BACKEND: In a scratchpad named exactly the artifact slug (use the
slugreturned bycreate_artifact/open_artifactas the scratchpad name), implement the backend code.launch_backendruns the backend in this same scratchpad's venv, so any packages you install or imports you test here will be present at launch.
CANONICAL TEMPLATE (use this skeleton verbatim, add your routes inside the # === API routes === block). It runs unchanged both locally (python backend.py --port=NNN) and on AWS Lambda (handler = backend.handler):
import argparse
import os
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from mangum import Mangum
app = FastAPI()
# CORS — frontend may be served from a different origin (e.g. CloudFront/S3
# in front of the Lambda). Tighten `allow_origins` in production.
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# === Secrets ===
# Keys are the canonical DS_<ENGINE>_<NAME>__<FIELD> env-var names. Locally
# each value comes from os.environ (the data vault injected it into Anton's
# env, which `launch_backend` inherits). In the cloud, the shared runner
# overlays the decrypted values onto this dict before each request. Leave
# SECRETS empty if the backend uses none. READ a secret by key AT ITS POINT
# OF USE (inside the route) — never copy a SECRETS value into a module-level
# variable at import time.
SECRETS = {
# "DS_POSTGRES_PROD_DB__PASSWORD": os.environ.get("DS_POSTGRES_PROD_DB__PASSWORD"),
}
# === State (durable storage) — INCLUDE THIS BLOCK ONLY FOR
# `fullstack-stateful-app`; OMIT it entirely for `fullstack-stateless-app`.
# STATE mirrors SECRETS: the cloud runner overlays {url, token} (a short-lived
# capability for the trusted state broker) before each request; locally it stays
# None and the SQLite driver is used. Declare the state KEY schema in
# `state_manifest.json` next to this file — generate it from the anton_state
# model, do NOT hand-write the JSON (see STATE MANIFEST below). Build the store
# AT POINT OF USE (inside a route), reading the current STATE — never at import time.
STATE = None
from anton_state import open_store
_STATE_DIR = Path(__file__).resolve().parent
def get_store():
return open_store(
state=STATE,
manifest_path=str(_STATE_DIR / "state_manifest.json"),
local_path=str(_STATE_DIR / ".anton_state.db"),
)
# === API routes ===
@app.get("/api/hello")
async def hello():
# Example secret use (read at point of use, not at import):
# pw = SECRETS["DS_POSTGRES_PROD_DB__PASSWORD"]
# Example STATE use (stateful only; build the store at point of use):
# store = get_store()
# await store.put({"pk": user_id, "sk": "profile", "name": name})
# item = await store.get(user_id, "profile")
return {"hello": "world"}
# Static mount MUST come AFTER all API routes (mount at "/" catches every
# remaining path). Used for local preview; in Lambda, statics are served
# by an external service (CloudFront/S3), so this mount is harmless there.
STATIC_DIR = Path(__file__).parent / "static"
if STATIC_DIR.exists():
app.mount("/", StaticFiles(directory=str(STATIC_DIR), html=True), name="static")
# CLOUD entry-point. lifespan="off" is REQUIRED — there is no
# long-lived process for FastAPI startup/shutdown.
# (Locally, `uvicorn.run(app, ...)` below serves the app directly.)
handler = Mangum(app, lifespan="off")
if __name__ == "__main__":
import uvicorn
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, required=True)
args = parser.parse_args()
uvicorn.run(app, host="127.0.0.1", port=args.port)
RULES (critical):
- Save the file as
<artifact_path>/backend.py— the filename, thehandlerattribute, and theSECRETSdict are load-bearing (the cloud runner overlays secrets ontobackend.SECRETSand invokesbackend.handler). Do NOT rename any of them. - Keep
Mangum(app, lifespan="off"). Withoutlifespan="off"Mangum warns and may fail cold start. - SECRETS: expose
SECRETSas a module-level dict, keyed by the canonicalDS_<ENGINE>_<NAME>__<FIELD>name, with each entry initialized fromos.environ.get(...)(the local default). The cloud runner overlays the decrypted values onto this same dict before each request. Read a secret AT ITS POINT OF USE —SECRETS["DS_..."]inside the route — and NEVER hoist it into a module-level variable at import time: the import runs before the overlay, so the cloud value would be missed. If a credential-backed resource (DB pool, API client) is needed, build it LAZILY on first request, never at module level. - ALL API endpoints MUST live under the
/api/*path prefix (e.g./api/items,/api/users/{user_id},/api/search). This is a hard contract between backend and frontend: it separates API traffic from the static mount at/, and lets edge routing (CloudFront behaviors, API Gateway path-based routing) split frontend vs backend traffic by prefix in production. NEVER expose routes at the root (e.g./items,/login) — they will collide with the static mount and break in deployment. - API routes MUST be registered BEFORE
app.mount("/", StaticFiles(...)). FastAPI matches in registration order — a mount at/swallows everything after it. - The backend MUST accept
--portvia argparse and bind to that port. NEVER hardcode the port —launch_backendpicks a free one and passes it in. - Prefer
async deffor I/O-bound routes (DB queries, external HTTP calls viahttpx.AsyncClient). Syncdefis fine for trivial CPU work, but sync blocking I/O inside an async app stalls the event loop. - LOCAL STATE (the ONE rule that differs between the two fullstack types):
fullstack-stateless-app: no local state of any kind survives a request. No module-level mutable caches that matter across requests (USERS = {},SESSIONS = []) — in Lambda these globals may or may not survive between invocations, never rely on them. Treat the filesystem as read-only and non-persistent: anything written is lost between requests and may fail outright depending on the host (Linux, Windows, or a read-only cloud sandbox). NEVER write to<artifact_path>at runtime, and never rely on a file surviving to a later request. If a request genuinely needs scratch space, use the OS temp dir viatempfileand treat it as ephemeral (gone the moment the request ends). ALL persistence goes through external data sources.fullstack-stateful-app: durable state goes through the platformSTATEstore (module-levelSTATE, built viaget_store()), which is a document/key-value model — suitable for LIGHT state (counters, settings, sessions, simple documents keyed by id). Declare the state KEY schema instate_manifest.jsonnext tobackend.py(see STATE MANIFEST below for the exact format — do NOT hand-invent it). For HEAVY/relational needs (joins, transactions, analytics, large data) use an EXTERNAL database via a connected data source instead — do not force it intoSTATE. Theanton_stateSDK is injected at runtime (do NOT add it torequirements.txt— see REQUIREMENTS below) and needs pydantic v2, which the mandatoryfastapidependency provides — always keepfastapiinrequirements.txt. Every other rule in this list still applies.
- LOGGING:
print()andlogging.getLogger(__name__).info(...)both go to CloudWatch in Lambda and tobackend.loglocally — no extra setup needed. - REQUIREMENTS: always save a
<artifact_path>/requirements.txtwith at minimum:
Add any other libraries the backend imports (one per line:fastapi mangum uvicornpkgorpkg==1.2).launch_backendreads this file and installs everything into the slug-named scratchpad's venv before spawning the process. Only simple lines are supported —-r,-e,--index-url, blank lines and#comments are ignored. NEVER listanton_stateinrequirements.txt— it is NOT a published package and the install will FAIL to resolve it (anton-state was not found in the package registry), aborting the launch. The STATE SDK is provided to the backend automatically at runtime, sofrom anton_state import open_storejust works without any dependency line. This is the ONLY import you leave out ofrequirements.txt. - Do NOT start the server inside the scratchpad — use
launch_backendin step 6. - DECLARE DATASOURCES: if
backend.pyreads anyDS_<ENGINE>_<NAME>__<FIELD>env var, callupdate_artifact(slug=<slug>, datasources=[...])immediately after writing the file. Pass a flat list of connection slugs (e.g.["postgres-prod_db", "hubspot-main"]); each slug MUST match a connection from theConnected Data Sourcessection of this prompt. This records the deployable's credential dependencies inmetadata.jsonso the artifact can be redeployed with the right env vars later. Skip this call only when the backend uses noDS_*vars at all. - STATE MANIFEST (
fullstack-stateful-appONLY):state_manifest.jsonis a SINGLE universal contract read by the local SQLite driver AND (client-side) by the cloud HTTP driver — the trusted broker is schema-agnostic. GENERATE it from theanton_statemodel instead of hand-writing JSON (this makes a malformed manifest impossible):
The manifest describes ONLY the KEY schema, never data fields. The resulting JSON is a FLAT objectfrom anton_state.schema import StateSchema, Attr StateSchema( pk=Attr(name="pk"), # partition key (always type "S") sk=Attr(name="sk"), # sort key — omit entirely if unused collections=["comments", "users"], # every Collection(store, "<name>") you use ).to_manifest(f"{artifact_path}/state_manifest.json"){version, pk, sk?, gsis?, ttl_attribute?, collections?}wherepk/skare{"name": ..., "type": "S"}— string keys only in v1. Do NOT wrap it inentities/attributes/partition_key/sort_key(a DynamoDB-CreateTable-style shape) and do NOT declare non-key attributes: those fail validation (StateSchema ... pk Field required) at the first request. Store the actual values freely viastore.put({...})at runtime — they need no schema entry. List everyCollection(store, "<name>")name incollections(this is NOT declaring data fields — it is the collection registry). Removing a name here when UPDATING an already-published artifact BLOCKS the publish (its stored data would be orphaned) — to change the set you must /unpublish first and publish again. - STATE STORE API (
fullstack-stateful-appONLY): thestorefromget_store()is a key-value store keyed by(pk, sk). PREFER theCollectionhelper for light state — it manages the sort key and defaults the partition:
Low-levelfrom anton_state import Collection todos = Collection(get_store(), "todos") await todos.put("id1", {"text": "buy milk"}) # pk defaults to one partition items = await todos.list() # all items in the collection n = await Collection(get_store(), "counters").increment("visits", field="n")storemethods (all async; NOscan()/ "list everything"):await store.get(pk, sk=None)→ one item orNoneawait store.put(item)→ write (dict MUST includepkand, if the schema has a sort key,sk);_vis set by the store — never set it yourselfawait store.delete(pk, sk=None)await store.query(pk, *, sk_prefix=None, filters=None, limit=None)→ items sharing partition keypk(NO secondary indexes in v1 — there is noindex=argument)await store.increment(pk, sk=None, *, field, by=1)→ atomic counter (use this for counters; do NOT hand-roll read-modify-write)await store.update(pk, sk=None, *, set_fields=None, add_fields=None, if_version=None)→ atomic partial update DESIGN KEYS AROUND ACCESS PATTERNS: every "list" must map to a singlequery(pk=...)(orCollection.list()). Do NOT call the store in a loop — collect with onequery. Do NOT wrap a STATE mutation (put/delete/increment/update) in your own retry loop: on a timeout the outcome is unknown and a retry can double-apply — surface the error instead.
- BUILD FRONTEND (if needed): In a separate scratchpad:
- Build a single-file HTML dashboard or web interface
- Include all CSS and JS inlined (no external file references)
- MANDATORY: call
recall_skill("build-html-dashboard")and apply its full HTML output contract to the frontend — it is the single source of truth for dashboard/chart HTML. Only if that skill cannot be recalled, fall back to these defaults: single self-contained HTML file; Apache ECharts via CDN for charts; dark theme #0d1117; responsive layout with a viewport meta tag. - Save the entry-point to
<artifact_path>/static/index.html(create thestatic/subfolder if needed). ANY additional frontend assets that don't end up inlined intoindex.html(separate CSS, JS, images, fonts, large data .js payloads, and any file the user uploaded or pasted that you bring into the artifact) MUST live under<artifact_path>/static/— never at the artifact root, since the backend only serves files fromstatic/and publishing bundles nothing else. - All backend endpoints MUST be called under the
/api/*prefix (matches the backend route convention from step 4). The frontend never calls bare paths like/items— always/api/items. - API base URL is supplied via a
<meta>tag so the same HTML works locally AND when deployed with frontend and backend on different origins (e.g. CloudFront/S3 + API Gateway/Lambda). Include this line in<head>:
Empty<meta name="api-base" content="">contentis the local default — fetch falls back to a relative path and hits the same FastAPI process that serves the page. At deploy time the publisher rewritescontent=""to the real API root (e.g.content="https://abc123.execute-api.us-east-1.amazonaws.com"). - Read the meta tag once at startup and prepend it to every API call. Use this exact pattern (or an equivalent helper) — do NOT scatter
document.querySelectorcalls across the codebase:const API_BASE = document.querySelector('meta[name="api-base"]')?.content || ""; const api = (path) => `${API_BASE}${path}`; // usage: fetch(api('/api/items')) - NEVER hardcode an absolute URL in the source — no
fetch('http://localhost:PORT/...'), nofetch('https://api.example.com/...'), noconst API_BASE = 'http://...'. The meta tag is the ONLY place the base URL is configured.
- LAUNCH THE BACKEND: Call the
launch_backendtool with the artifact's slug:
launch_backend(slug=<slug>)— the tool picks a free port, spawnspython backend.py --port <port>as a standalone process with<artifact_path>as cwd, waits for readiness, writes the port intometadata.json, and returns a JSON envelope: the URL inexternal_urland{slug, port, pid, log_path}underdetails.- Uses the scratchpad named
<slug>— created automatically on first call. If<artifact_path>/requirements.txtexists, its packages are installed into that scratchpad's venv before spawn (install output is appended tobackend.logwith a banner). An install failure aborts the launch and is returned as an error string — fixrequirements.txtand retry. - Backend stdout/stderr stream to
<artifact_path>/backend.log— read it if the launch fails or the API misbehaves. - Do NOT call
update_artifact(port=...)manually —launch_backenddoes it. - The launched process outlives the scratchpad cell and is reaped automatically when the Anton session ends.
- Calling
launch_backendagain for the same slug terminates the previous process and starts a fresh one — use this for hot reloads after code changes.
- PREVIEW THE APPLICATION: Direct the user to the
external_urlreturned bylaunch_backend(e.g. http://127.0.0.1:54321):
- CRITICAL: Open that URL, NOT the HTML file from disk (file://...). The backend serves the frontend at
/, so opening the URL loads the page and itsfetch()calls land on the same origin. - If the user opens the HTML file directly from disk,
fetch()calls fail due to browser CORS/file:// restrictions.
DEPLOYMENT NOTES:
- Same
backend.pyruns in two modes:- LOCAL:
python backend.py --port=NNN(used bylaunch_backend). uvicorn serves the FastAPI app and thestatic/mount, frontend reachable at/. Secrets come from theDS_*env vars inSECRETS' defaults. - CLOUD: a shared runner overlays the decrypted secrets onto
backend.SECRETSand invokesbackend.handler(the Mangum ASGI app) per request. Statics are served separately (the gateway readsstatic/from object storage), so theStaticFilesmount sits unused there — the runner only sees/api/*traffic.
- LOCAL:
- Secrets ride in the backend module's
SECRETSdict, notos.environ— the shared cloud runner injects them per request without polluting the process env. - The local backend process shuts down when the Anton CLI session ends (per MVP constraints).
PUBLISH OR SHARE:
- After building, offer to preview the frontend by directing the user to the
external_urlreturned bylaunch_backend - The backend must be running for the frontend to work
版本历史
-
18b043f
当前 2026-09-03 08:35
将后端生成提示词重构为内置技能,优化系统提示词体积;修复技能召回幂等性及阴影回退问题。
- 0f2b69b 2026-08-20 00:13


