TECHIN 510 — Spring 2026

APIs, Databases &
the Full-Stack Transition

Week 5: From Streamlit to Next.js + Supabase
University of Washington • Global Innovation Exchange
01 / 33
What You Will Learn

Learning Objectives

1
Explain why we transition from Streamlit to Next.js + Supabase Name at least three things Streamlit cannot do
2
Justify a tech stack choice based on user requirements Not personal preference or familiarity
3
Read TypeScript by mapping Python equivalents deffunction, const/let, unions & optional fields, interface / type
4
Diagram the three-tier architecture Browser ↔ Server ↔ Database — contrast with single-tier
02 / 33
What You Will Learn

Learning Objectives (continued)

5
Call a REST API and parse the JSON response Define an API and when to use it (server vs. client); use curl and Python requests, handle error cases
6
Insert and query data from Supabase Contrast database categories and why relational/Postgres fits this course; explain why data persists after the process terminates
7
Identify security vulnerabilities in code Hardcoded keys, leaking error internals, client-side secrets
8
Use Claude Code in Plan Mode to scaffold a Next.js project Evaluate which scaffold decisions required human judgment

Today: lecture + demo (55 min) then hands-on lab (105 min)

03 / 33

The Transition

Why we change tech stacks
04 / 33
The Limitation

What Streamlit Cannot Do

1
User accounts No built-in authentication. Each session is a sealed box with no connection to any other.
2
Persistent data Close the app, lose the data. No database, no memory between sessions.
3
Multi-user access Two tabs, two separate worlds. No shared state between users.

These are not edge cases. They are the core requirements of your final project.

05 / 33
Systems Thinking

Architecture Bridge

Single-Tier (Weeks 1–4)

One file: app.py
UI + logic + data access all together.
Fast to prototype, hard to scale.

Three-Tier (Weeks 5+)

Presentation • Application • Data
Same responsibilities, separated for
scale, security, and team collaboration.

Weeks 1–4 ArtifactWeeks 5+ Artifact
CSV schema / format contractSQL schema + migrations
DataFrame assertRow-level security (RLS) policy
st.metric()React component
06 / 33

Tech Stack Decision

Active learning: you predict first
07 / 33
Think-Pair-Share

Which Stack Wins?

Scenario Streamlit? Next.js?
GIX study-space sensor dashboard — one researcher, read-only, no login Yes Over-engineering
Shared incident-tracking app — login required, per-user data Fighting the framework Yes
Quick prototype to test if users want a bookmark feature Yes Too much infra for a test

Invest production infrastructure only after you have validated the idea.

08 / 33

The TypeScript Bridge

Same concepts, different punctuation
09 / 33
Syntax Equivalence

Python ↔ TypeScript

Python
def greet(name: str) -> str:
    return f"Hello, {name}!"

user = {"name": "Alice", "age": 25}
print(user["name"])

numbers = [1, 2, 3]
doubled = [n * 2 for n in numbers]
TypeScript
function greet(name: string): string {
  return `Hello, ${name}!`;
}
const user = { name: "Alice", age: 25 };
console.log(user.name);

const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2);

deffunction • Indentation → curly braces • f-strings → backtick template literals • List comprehension → .map()

10 / 33
Syntax Equivalence

Async & Data Fetching

Python
import requests

response = requests.get(url)
data = response.json()
TypeScript
const res = await fetch(url);
const data = await res.json();

Key insight: async/await in TypeScript looks almost identical to Python. In web development, every API call and database query is asynchronous — the pattern becomes second nature fast.

11 / 33
Your Safety Net

TypeScript Types & Interfaces

Without Types (JavaScript)
function greet(user) {
  return `Hello, ${user.name}`;
}

greet({ username: "Alice" });
// Returns "Hello, undefined"
// Silent bug at runtime
With Types (TypeScript)
interface User {
  name: string;
  email: string;
}

function greet(user: User): string {
  return `Hello, ${user.name}`;
}

greet({ username: "Alice" });
// ERROR at compile time!
// "Did you mean 'name'?"

An interface is a contract: "anything labeled as a User must have these fields." When AI generates code that passes the wrong shape, TypeScript catches it before you run anything.

12 / 33
Reading Interfaces

The Todo Interface

// This interface says: every Todo from the database
// has exactly these fields.
export interface Todo {
  id: string;          // UUID from Supabase
  user_id: string;     // who owns this record
  title: string;       // the content
  is_complete: boolean;
  created_at: string;
}

When you see an interface, translate it as: "here is the shape of data that flows through this boundary." If AI accidentally calls todo.body instead of todo.title, TypeScript tells you right now — not when a user complains.

13 / 33
TypeScript

Syntax You Will See in Starter Kits

Variables
const name = "Ada";   // default: cannot reassign
let count = 0;      // when reassignment is needed

Think Python: const ≈ a name that does not get rebound; let when you update the value.

Primitives & arrays
// primitives
const n: number = 42;
const ok: boolean = true;
const label: string = "hi";

// array of strings
const tags: string[] = ["ui", "lab"];
Objects & optional fields
interface Profile {
  name: string;
  email?: string;   // optional — may be absent
}

// inline object type (common in callbacks)
function onSave(data: { title: string; id?: string }) { /* ... */ }

Reading order: types sit to the right of : on variables and parameters. The editor uses them to flag mistakes before you run the app.

14 / 33
TypeScript

Unions, Narrowing, and type

Union types & narrowing
type Status = "idle" | "loading" | "error";

function fmt(value: string | null): string {
  if (value === null) return "";
  // TS knows `value` is string here
  return value.toUpperCase();
}
interface vs type

interface — named object shape; common as export interface Props. Often extended.

type — alias for unions, primitives, tuples: type ID = string;

Narrowing: after if (x === null) return;, TypeScript treats x as defined below. Same idea as checking response.ok before response.json().

15 / 33
Mental Model Shift

Hooks vs. Streamlit Re-Run

Streamlit

Every interaction → entire script re-runs from top to bottom.

Simple, but cannot build real-time interactive UIs.

React (Hooks)

Only the affected component re-renders when its state changes.

const [todos, setTodos] = useState([]);
// setTodos(newData) → re-renders
// ONLY this component

useState declares reactive state. useEffect runs code after the component appears (like "on_load"). You don't need to master these — just recognize them in the starter kit.

16 / 33
Next.js 15

Server vs. Client Components

Server Component (default)

Runs on the server before the browser receives HTML.

  • Fetch from Supabase safely
  • Access server env vars
  • Run database queries

Cannot: useState, useEffect, onClick

Client Component
"use client"; // ← this line matters

Runs in the user's browser.

  • Use useState, useEffect
  • Respond to clicks, typing
  • Call client-side APIs

Cannot: access server secrets safely

Rule of thumb: Add 'use client' only when you need useState, useEffect, or event handlers. Otherwise, keep it as a Server Component.

17 / 33

Multi-Tier Architecture

From one file to three layers
18 / 33
Architecture Shift

Single-Tier → Three-Tier

WEEKS 1-4 app.py UI Business Logic Data Access Everyone sees everything WEEKS 5-10 BROWSER React / Next.js — User Interface HTTP / JSON SERVER Next.js API Routes — Secrets live here SQL DATABASE Supabase — Persistent Storage + RLS

Standard tier names: Browser (Presentation) → Server (Application) → Database (Data). Arrows between tiers are boundaries. Mixing tiers = “framework mismatch” anti-pattern.

19 / 33
APIs

What Is an API?

1
A contract between programs It defines how one piece of software requests data or actions from another over a network — not the same as an HTML page meant for humans to read.
2
Request and response The client sends a method (GET, POST, …), a URL, optional headers and body. The server replies with a status code and a body (often JSON).
3
Browsers vs. APIs Navigating to a site returns HTML for rendering. Calling an API returns machine-readable data (usually JSON) for your code to parse — the same workflow you have used since Week 1.
20 / 33
APIs

When and How to Use APIs

When
  • Pull in third-party data (weather, maps, payments)
  • Expose your backend to web, mobile, or partners
  • Keep secrets on the server (API keys, LLM calls)
  • Define service boundaries between systems
How
  • Choose an HTTP method: GET read, POST create, PUT/PATCH update, DELETE remove
  • Send auth in headers (e.g. Authorization: Bearer …) — not query strings for secrets
  • Parse JSON; handle errors and timeouts (following slides)

This course: use fetch to public HTTPS APIs or to your Next.js routes. Private keys stay in Server Components or API routes — same rule as “where would an agent live?”

21 / 33
Phase A

Raw API Call with curl

curl "https://api.open-meteo.com/v1/forecast?\
  latitude=47.65&longitude=-122.30&\
  hourly=temperature_2m&forecast_days=1"
1
Concrete URL + JSON response Open-Meteo returns the same JSON shapes you have parsed since Week 1
2
The source changed, the pipeline did not Recall Week 3: 9 stages from sensor to chart. An API compresses stages 1–4 into one call.
22 / 33
Phase B

Python with Error Handling

response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()

# Three error cases — not one:
except requests.exceptions.Timeout:
    # Server slow or unreachable
except requests.exceptions.HTTPError as e:
    # Server replied with 4xx or 5xx
except requests.exceptions.ConnectionError:
    # Network itself failed

Same defensive mindset as Week 3: external systems fail. raise_for_status() makes failures loud — a blank chart three interactions later is your enemy.

23 / 33
Testing & Validation

API Test Matrix

ScenarioStatusWhat to Assert
Happy path 200 OK Response body matches expected schema
Auth failure 401 No data leaked, clear error message
Network error timeout UI shows fallback, no crash
Malformed input 400 Validation error returned, not a 500
// Your first integration test
const res = await fetch('/api/items', { method: 'GET' });
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toHaveProperty('items');

Your first integration test. Four scenarios, four assertions.

24 / 33
Data Layer

What Is a Database?

1
Persistent, structured storage Data survives process restarts and deploys. A schema describes tables or collections; a query language (often SQL) reads and updates rows reliably.
2
More than a file or in-memory list Databases support many concurrent users, enforce rules, and (for relational systems) transactions so related changes commit or roll back together.

If closing Streamlit wipes your state, you need a database (or equivalent hosted storage) for multi-user or durable apps.

25 / 33
Data Layer

Categories & When to Use Them

CategoryExamplesTypical use
Relational / SQL PostgreSQL (Supabase), SQLite Structured rows, joins, ACID, multi-user apps
Document MongoDB Flexible JSON-shaped documents, rapid iteration
Key-value / cache Redis Sessions, rate limits, speed
Vector pgvector, dedicated vector DBs Embeddings, semantic search (later weeks)
When (this course)

Default to relational for durable state, relationships between entities, auth-linked rows, and shared access — aligned with the final project.

How

Use a connection URL and a client or SQL; evolve the schema with migrations. Next: Supabase = hosted Postgres + auth + RLS.

26 / 33
Phase C

Supabase — Persistence

PostgreSQL via Supabase: the code below uses the same relational model as the previous slides. Supabase adds hosted Postgres, auth, and row-level security.

supabase = create_client(url, key)

# Read all bookmarks
result = supabase.table("bookmarks").select("*").execute()

# Insert a new bookmark
supabase.table("bookmarks").insert({
    "url": "https://api.open-meteo.com",
    "title": "Open-Meteo API Endpoint",
}).execute()

# Read again — one more row
result = supabase.table("bookmarks").select("*").execute()

The key demo: Kill the terminal. Count to three. Reopen. Data is still there. That is persistence — the gap between Streamlit and a production web application.

27 / 33

Security Architecture

Three rules for Weeks 5–10
28 / 33
Whiteboard Rules

Security Rules

1
No hardcoded secrets — ever Use os.environ["KEY"] with square brackets. Crash on startup if missing — intentional.
2
.env in .gitignore — always Bots scan every public commit for API keys. If exposed: rotate the key before anything else.
3
Error messages do not leak internals Log str(e) server-side. Return a generic message to the client.
Wrong
API_KEY = "sk-proj-abc123xyz789"
requests.get(url,
  params={"api_key": API_KEY})
Right
api_key = os.environ["OPENAI_API_KEY"]
requests.get(url,
  headers={"Authorization":
    f"Bearer {api_key}"})
JWT Anatomy: header.payload.signature
The header says which algorithm, the payload carries user data (like user ID and email), and the signature proves it hasn't been tampered with. Supabase creates and verifies JWTs automatically — you never build them by hand.
29 / 33
Active Learning

Find the Vulnerability

Three snippets. Each has a different security bug. 30–35 seconds per snippet.

Snippet A — Warm-up
API_KEY = "sk-proj-abc123xyz789def456"

response = requests.get(
    "https://api.openai.com/v1/models",
    params={"api_key": API_KEY}
)

Hardcoded key + key in URL params (appears in server logs)

Snippet B — Moderate
except Exception as e:
    return jsonify({"error": str(e)}), 500

str(e) leaks database hostname, project URL, and connection params to the client

30 / 33
The Trap

Snippet C: NEXT_PUBLIC_

'use client'                   // ← runs in the browser

export default function Dashboard() {
  const fetchData = async () => {
    const res = await fetch(
      `https://api.example.com/data?key=${process.env.NEXT_PUBLIC_SECRET_KEY}`
    );
    return res.json();
  };
}

NEXT_PUBLIC_ means "compile into the client JavaScript bundle." Anyone can open DevTools → Sources and find it in 30 seconds.

The rule: If the variable is a secret, it lives in a Server Component, API Route, or Server Action. It never gets a NEXT_PUBLIC_ prefix.

31 / 33
The 40/20/40 in Action

AI Did Well vs. Human Judgment

AI Did Well
  • Project scaffolding
  • File structure creation
  • Tailwind CSS styling
  • Boilerplate API routes
  • Type annotations
  • Import statements
  • Repetitive CRUD code
Human Judgment Required
  • Server vs. Client Component
  • Which routes need auth
  • Row Level Security design
  • Error message content
  • .env variable naming
  • When to use Supabase vs. API Route
  • Data model & schema design

If you skip the spec, the AI populates the right column with guesses. Some will be correct. Some will create security vulnerabilities.

32 / 33
The Takeaway

The Spec Is
the Architecture

"A structural engineer does not pick the paint color. An interior designer does not specify the load-bearing wall dimensions. Claude Code is the structural engineer. Cursor is the interior designer. You are the architect."

Write the spec before touching any AI tool. The 40% planning determines whether the 20% coding produces the right thing.

33 / 33