Across the Boundary — Cheat Sheet
Full-Stack Integration · 3 topics. Download the PDF or the Instagram carousel and share it.
Cheat Sheet · AiCanCode.org
Across the Boundary
Full-Stack Integration3 topicsQuick revision reference
1
File Upload, End to End
Small files can go through your API. Anything larger should not — the browser uploads directly to storage with a signed URL, and your server only signs and records.
- ✓Uploading through your API costs request-size limits, memory and doubled bandwidth; direct-to-storage avoids all three
- ✓The signing endpoint is the security boundary — it authenticates, constrains type and size, and picks the key
- ✓The client's declared content type is attacker-controlled; verify the actual bytes server-side
- ✓Verify the object exists after a confirmation, and sweep signed-but-never-confirmed orphans
- ✓Private files need short-lived signed download URLs issued after an ownership check, never a permanent public URL
Verify the bytes, stream, and know the caps
// Client — multipart, and no Content-Type header
const form = new FormData()
form.append('file', file)
await fetch('/api/avatar', { method: 'POST', body: form,
credentials: 'include' })
// Setting Content-Type manually breaks the multipart boundary.
# FastAPI
@router.post("/avatar")
async def upload(file: UploadFile, user = Depends(current_user)):
if file.content_type not in {"image/jpeg", "image/png", "image/webp"}:
raise AppError(422, "bad_type", "JPEG, PNG or WebP only")
head = await file.read(8) # sniff the real bytes —
await file.seek(0) # the client's content_type
if not is_image_magic(head): # is attacker-controlled
raise AppError(422, "bad_type", "That is not an image")
# stream to disk/storage, never file.read() the whole thing
key = f"avatars/{user.id}/{uuid4()}.webp"
await storage.upload_stream(key, file)
return {"url": public_url(key)}
// The limits you are now inside: Vercel functions cap the request
// body (a few MB), Fly and nginx have their own, and the whole file
// occupies your server while it transfers.2
Pagination, Filtering and Sorting
Offset pagination is simple and drifts under writes; cursor pagination is stable and cannot jump to page seven. Both need the same discipline about what the client is allowed to ask for.
- ✓Offset pagination drifts when rows are inserted and degrades at depth; cursor pagination is stable and constant-cost
- ✓A cursor must be opaque and encode the sort, since changing the sort mid-pagination otherwise returns nonsense
- ✓Never interpolate a client-supplied sort column into SQL — allow-list it and cap the page size
- ✓Every sortable column needs an index matching the ORDER BY, tiebreaker included
- ✓Filter and paginate on the server, keep the page in the URL, and reset to page 1 whenever a filter changes
Offset for pages, cursor for feeds and scale
# OFFSET — page numbers, a total count, jump anywhere
GET /problems?page=2&limit=20
{ "items": [...], "page": 2, "limit": 20, "total": 137 }
SELECT * FROM problems ORDER BY created_at DESC LIMIT 20 OFFSET 20;
# + numbered pages, "showing 21-40 of 137"
# - a row inserted at the top while reading shifts everything, so
# page 2 repeats an item from page 1
# - OFFSET 100000 makes the database count and discard 100,000 rows
# CURSOR — keyset, stable, and fast at any depth
GET /problems?limit=20&cursor=eyJpZCI6NDIsInRzIjoi...
SELECT * FROM problems
WHERE (created_at, id) < (:last_created_at, :last_id) -- id breaks ties
ORDER BY created_at DESC, id DESC
LIMIT 21; -- one extra: is there more?
# + no drift, constant cost at any depth
# - no page numbers, no jumping, and a total is a separate query
# Choosing: an admin table people navigate by page -> offset.
# An infinite feed, or a table with millions of rows -> cursor.
# The cursor must be opaque (base64 of the sort key), so its shape
# can change later — and it must encode the sort, or changing the
# sort mid-pagination returns nonsense.3
Long-Running Operations
Anything slower than a few seconds should not be an HTTP request. Accept the work, return an id, and report progress separately.
- ✓Work longer than a few seconds must not run inside an HTTP request — gateways and proxies will cut it off
- ✓Accept the job, return 202 with an id and a status URL, and process it in a worker
- ✓Workers can be redelivered, so every job must be idempotent and safe to run twice
- ✓Poll with increasing intervals for most job UIs, SSE when live progress matters, webhooks between servers
- ✓Put the job id in the URL so the user can leave and come back, and time out stuck jobs or the UI polls forever
202 with a job id, status on a separate endpoint
# Do NOT do the work in the request
@router.post("/reports")
async def create_report(body: ReportIn, user = Depends(current_user)):
job = await db.create_job(kind="report", user_id=user.id,
params=body.dict(), status="queued")
await queue.enqueue("generate_report", job.id) # Celery, RQ, Arq…
return JSONResponse(202, {"job_id": job.id,
"status_url": f"/jobs/{job.id}"})
@router.get("/jobs/{job_id}")
async def job_status(job_id: str, user = Depends(current_user)):
job = await db.get_job(job_id, user_id=job_id and user.id) # ownership
if not job: raise HTTPException(404)
return {"status": job.status, # queued | running | done | failed
"progress": job.progress, # 0-100, if you can compute it
"result_url": job.result_url, # when done
"error": job.error} # when failed
# The worker updates progress as it goes, so the UI can show
# something honest rather than an indeterminate spinner for a minute.
# Jobs must be idempotent and retryable: a worker can die mid-task
# and the queue will redeliver. Design for the task running twice.Learn this free with Aria, your AI tutor → AiCanCode.org/learn/full-stack