Minimal, mobile‑first language practice. Built for speed, clarity, and a delightful UX.
App Router • API routes • Prisma ORM • SQLite • Standalone build • One‑container deploy
- Bold, mobile‑first UI with accessible focus states and toasts
- Next.js App Router with server components and
app/api/*endpoints - Prisma + SQLite for a zero‑ops, file‑based database that “just works”
- One‑command local setup; instant seed/reset scripts for fast iteration
- Production‑ready Dockerfile using Next standalone output
- Next.js 15, React 19, TypeScript 5
- Prisma 6 (SQLite), Node.js 20
- Bun (dev ergonomics,
bunxfor Prisma) - Vanilla CSS (no framework) tuned for touch + desktop
Prereqs: Node 20+ (or Bun), Docker optional.
- Install deps
# Recommended
bun install
# or npm
npm install- Configure the database URL (SQLite file path). Create
.envin the repo root:
DATABASE_URL="file:../data/ling.db"- Create the database and generate the Prisma client
# Push schema (skips generate in script); dev regenerates client automatically
bun run db:push- Start the dev server
bun run dev
# http://localhost:3000- Seed sample data (languages, units, prompts)
bun run db:seed
# Or recreate from scratch (delete DB file, push, seed)
bun run db:resetHealth check: GET /api/health
- Endpoint:
GET /metrics(no auth). Returns Prometheus text format with default Node/process metrics and custom HTTP metrics. - Enable/disable: set
METRICS_ENABLED=true|false(defaulttrue). When disabled,GET /metricsreturns404and request instrumentation is a no-op. - Labels:
serviceis set fromAPP_NAMEorpackage.jsonname. HTTP metrics are labeled bymethod,route,status, andservice. - Counters/Histograms:
http_requests_total{method,route,status,service}http_request_duration_seconds{method,route,status,service}with sensible buckets
- Note: Place a reverse proxy rule to block public access to
/metrics. It is intended for internal scraping by Prometheus.
GET /api/langs— list languagesGET /api/units/:langId— units for a languageGET /api/prompts/:unitId— prompts for a unitPOST /api/create/lang—{ name }POST /api/create/unit—{ langId, name }POST /api/create/prompt—{ unitId, content, answer }
Build the image:
docker build -t ling-script-next .Run with a bind mount to persist the SQLite file on the host:
# Linux/Mac
docker run --name ling-next -p 3000:3000 \
-v "$(pwd)/data:/app/data" \
-e DATABASE_URL="file:/app/data/ling.db" \
ling-script-next# Windows PowerShell
docker run --name ling-next -p 3000:3000 `
-v ${PWD}\data:/app/data `
-e DATABASE_URL="file:/app/data/ling.db" `
ling-script-nextNotes:
- SQLite lives at
/app/data/ling.dbinside the container; no external DB needed - You can change the filename/location by adjusting
DATABASE_URL - If you mount a host folder to
/app/data, ensure it is writable by the container user. On Linux:sudo chown -R 1000:1000 ./data
app/ # App Router (pages + API routes)
api/ # REST-ish endpoints
lang/[id]/ # Language units listing
unit/[id]/ # Practice screen (client)
components/ # UI primitives (TopNav, Toasts, etc.)
lib/prisma.ts # Prisma client (single instance)
prisma/schema.prisma # Data model (Lang, Unit, Prompt)
prisma/seed.js # Seed script (idempotent)
scripts/reset-db.js # Quick nuke & seed for dev
Dockerfile # Multi-stage, standalone Next build
dev—bunx prisma generate && next devbuild—prisma generate && next buildstart—next startdb:push—prisma db push --skip-generatedb:seed—bun prisma/seed.jsdb:reset— remove DB, push schema, seed
- Richer progress tracking and spaced repetition
- Export/import user progress
- More languages and curated units
Made with care. If this helped you evaluate my skills, I’m happy to walk through the code, decisions, and tradeoffs in an interview.