JSON APIs
uv run stk new <module> writes a complete, working set of these endpoints in stk/<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 the global error handler in 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.

