API Development
Caspian is built on top of FastAPI. Whether you need a dedicated backend service or hybrid API routes within your fullstack app, you have native access to high-performance async endpoints.
"backendOnly": true, Caspian exposes
FastAPI's automatic Swagger, ReDoc, and OpenAPI routes. In a full-stack app
("backendOnly": false), those routes are not
registered, leaving /docs available to the app.
Backend-Only Mode
Best for when you want a standalone API service without the templating engine. This enables standard FastAPI features like automatic Swagger UI documentation by default.
Swagger UI
Automatic interactive docs available at /docs.
Lightweight
Disables the HTML rendering engine for maximum raw throughput.
API Endpoints With route.py
A folder can be an HTTP endpoint instead of a page: put a
route.py
there and define one function per HTTP method
(get, post, put,
patch, delete, head,
options). This is the recommended way to serve callers that are not your
PulsePoint UI — webhooks, mobile apps, third-party integrations, public JSON feeds.
# src/app/api/orders/route.py -> /api/orders
from casp.errors import HttpError
async def get(request, limit: int = 20):
return {"orders": await list_orders(limit)}
async def post(request):
data = await request.json()
if not data.get("sku"):
raise HttpError(422, "sku is required")
return await create_order(data), 201
- Arguments follow the
page()contract: path params as one positional dict,requestby keyword, query params by name with type coercion. Read bodies withawait request.json()orawait request.form(). - Returns: a dict, list, dataclass, or model is JSON;
(body, status)or(body, status, headers)sets the status;Noneis204; aResponsepasses through; a generator streams as SSE.
- Only functions defined in the file become endpoints, so an imported
getis never exposed. An undefined method answers405;HEADfalls back toget. - No layouts, metadata, or page cache. Failures are
application/problem+json— see Error Handling. Route privacy, rate limiting, and security headers apply exactly as for pages. - A folder is either
index.pyorroute.py, never both.
CSRF: POST/PUT/PATCH/DELETE
requests that carry the browser session cookie must send the token in X-CSRF-Token
(or a csrf_token form field). Requests without the session cookie — webhooks and
server-to-server clients — are not checked, and cannot act as a signed-in user either. Set
csrf = False only for an endpoint that authenticates another way, such as a signed webhook.
# src/app/api/webhooks/stripe/route.py
csrf = False # authenticated by the provider signature instead
async def post(request):
payload = await request.body()
verify_signature(payload, request.headers.get("stripe-signature"))
return None # 204 No Content
@rpc() plus
pp.rpc(). route.py exists for external callers,
not as a substitute for RPC.
Fullstack Hybrid Routes
A page's page() can also bypass the HTML renderer.
Return a JSONResponse
from your page function to bypass the HTML renderer.
from fastapi import Request from fastapi.responses import JSONResponse # This function runs when you visit /api/users def page(request: Request): # Perform your logic (DB queries, etc.) users = [ "id": 1, "name": "Jefferson", "id": 2, "name": "Alice" ] # Return JSONResponse to bypass Caspian's HTML renderer return JSONResponse(content="data": users)
page() function.
If it sees a FastAPI Response object (like JSONResponse), it skips the Jinja2 template engine entirely,
giving you raw API performance.
Choosing HTTP Methods
Every route Caspian registers accepts
GET
and
POST
by default. To narrow or widen that, export a module-level
route_methods
list from the route's index.py. Caspian reads
it at registration time, uppercases and de-duplicates the entries, and
passes them straight to
app.add_api_route(...). Anything else gets
the usual FastAPI 405 Method Not Allowed.
from fastapi import Request from fastapi.responses import JSONResponse # Only accept POST on this endpoint route_methods = ["POST"] async def page(request: Request): payload = await request.json() return JSONResponse(content="received": True)
route_methods is read from
index.py only, so an HTML-only route always
keeps the GET/POST
default. Empty or non-list values are ignored rather than treated as
“no methods”.