JSON APIs
uv run stk new <module> writes a complete, working set of these endpoints in myapp/<module>/views.py. Start from that file; this page explains its conventions.
List endpoints
Section titled “List endpoints”Lists paginate manually and serialize with orjson. The response shape is {items, total, perPage}, which is what the frontend’s server-paginated table expects.
import orjson as jsonfrom quart import Response, g, requestfrom sqlalchemy import func, or_, select
PER_PAGE = 25
@bp.get("/api/posts")async def list_posts(): page = request.args.get("page", 1, type=int) per_page = request.args.get("per_page", PER_PAGE, type=int)
query = select(Post) if search := request.args.get("search"): query = query.where( or_(Post.title.ilike(f"%{search}%"), Post.content.ilike(f"%{search}%")) )
total = await g.db_session.scalar( select(func.count()).select_from(query.subquery()) ) result = await g.db_session.execute( query.offset((page - 1) * per_page).limit(per_page) ) items = [p.to_dict() for p in result.scalars().all()] return Response( json.dumps({"items": items, "total": total, "perPage": per_page}), content_type="application/json", )Count from query.subquery() so filters apply to the total too.
Mutations
Section titled “Mutations”The frontend sends {item: {...}}. Create is POST /api/post/, update is POST /api/post/<id> (POST, not PUT), delete is DELETE /api/post/<id>. Admin mutations are logged with Activity.register.
@bp.post("/api/post/")async def create_post(): data = (await request.json).get("item", {}) if not data.get("title"): return {"message": "Title is required"}, 400 post = await Post().from_dict(data) g.db_session.add(post) try: await g.db_session.flush() # assigns post.id for the audit entry await Activity.register(current_user.id, "Post Create", post.to_dict()) await g.db_session.commit() return {"message": "Post created"} except Exception: await g.db_session.rollback() log.exception("Error creating post") return {"message": "Error creating post"}, 412Responses are {"message": ...}: 400 for invalid input, 404 for a missing row, 412 for a failed write. An unhandled exception reaches stk’s global error handler (stk/app.py), which rolls back the session and returns JSON for API requests.
Protecting endpoints
Section titled “Protecting endpoints”Put auth on the blueprint so no route can forget it:
@bp.before_request@auth_required("session")@roles_required("admin")async def before_request(): passuv run stk inspect routes --json reports which routes are guarded, and stk verify fails if a route reported as guarded is reachable anonymously.
CSRF and webhooks
Section titled “CSRF and webhooks”Every mutation requires a CSRF token, which Axios sends from the shared layout token. A third-party webhook without a session opts out with @csrf_exempt from stk.security, placed under the route decorator, and must authenticate the caller itself, for example by checking an HMAC signature. See Authentication.

