Cheat SheetsFull-Stack IntegrationAcross the Boundary

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