LearningLab lets educators and parents generate a reading-comprehension worksheet (passage, five multiple-choice questions, two short-answer questions and an answer key) for any K-12 grade level and topic, using OpenAI's gpt-5-mini model, and print it straight from the browser.
- Grade-level specific (K-12) - vocabulary and complexity tailored to the selected grade
- Topic-based - pick one of the curated topics or type your own
- Print-ready output - clean worksheet and answer-key layout via the browser's print dialog
- Built-in abuse controls - per-IP rate limiting, request size limits, topic filtering
LearningLab/
├── server/ # Express API (TypeScript, ESM) — used for local dev / self-hosting
│ ├── src/
│ │ ├── config/ # env loading, OpenAI settings
│ │ ├── controllers/ # /api/generate-worksheet handler
│ │ ├── middleware/ # validation, rate limiting, error handling
│ │ ├── routes/
│ │ └── services/ # OpenAI call + prompt
│ └── client/ # React 19 + Vite 8 + Tailwind CSS 4 front end (own lockfile)
├── functions/api/ # Cloudflare Pages Functions — the production API
├── test/ # Tests for the Cloudflare function
├── wrangler.toml # Cloudflare Pages configuration
└── .github/ # CI (typecheck, lint, tests, build, smoke test) and Dependabot
The front end is a static Vite build. In production it is hosted on Cloudflare Pages and calls functions/api/generate-worksheet.js on the same origin. For local development (and for self-hosting on a Node server) the same client is served by the Express app in server/, which exposes an equivalent POST /api/generate-worksheet endpoint. The two API implementations share the same request validation rules and rate limits; keep them in sync when changing either.
| Layer | Technology |
|---|---|
| Front end | React 19, Vite 8, Tailwind CSS 4, Headless UI, Heroicons |
| Local/self-hosted API | Node.js 22, Express 5, TypeScript, Zod, express-rate-limit, express-slow-down |
| Production API | Cloudflare Pages Functions + Workers KV (rate limiting) |
| AI | OpenAI Node SDK (gpt-5-mini, JSON mode) |
- Node.js 22.12+ (see
.node-version;nvm usepicks it up) and npm 10+ - An OpenAI API key
git clone https://github.com/southbayjay/LearningLab.git
cd LearningLab
npm install # root workspace + server
npm install --prefix server/client # client has its own lockfileThe Express server reads server/.env (not the repo root):
cp .env.example server/.env
# then set OPENAI_API_KEY=sk-... in server/.envnpm run dev- Express API on
http://localhost:3001 - Vite dev server on
http://localhost:5173(hot reload; calls the API on 3001 directly)
npm run build # builds the client into server/client/dist
npm run build:server # compiles the server into server/dist
NODE_ENV=production npm startExpress serves the built client and the API from http://localhost:3001. When running behind a reverse proxy set TRUST_PROXY (see .env.example) so rate limits apply per client IP rather than per proxy.
npm test # Cloudflare function tests + server tests
npm run lint --prefix server # ESLint (auto-fix)
npx tsc --noEmit -p server/tsconfig.eslint.json| Script | Description |
|---|---|
npm run dev |
Start API (3001) and client (5173) with hot reload |
npm run dev:server / npm run dev:client |
Start one side only |
npm run build |
Build the client (server/client/dist) |
npm run build:server |
Compile the server (server/dist) |
npm start |
Run the compiled server |
npm test |
Run all tests |
Set these in server/.env for the Express server, or in the Cloudflare Pages dashboard (Settings → Environment variables / Bindings) for production.
| Variable | Used by | Default | Description |
|---|---|---|---|
OPENAI_API_KEY |
both | - | Required. OpenAI API key |
OPENAI_BASE_URL |
both | OpenAI | Override the OpenAI endpoint (testing / proxies) |
PORT |
Express | 3001 |
Listen port |
NODE_ENV |
Express | development |
production enables static serving and error redaction |
CORS_ORIGIN |
Express | http://localhost:5173 |
Allowed browser origin in development |
TRUST_PROXY |
Express | - | Express trust proxy setting when behind a reverse proxy |
WORKSHEET_RATE_LIMIT_MAX_REQUESTS |
Express | 10 |
Worksheets per IP per window |
WORKSHEET_RATE_LIMIT_WINDOW_MS |
Express | 3600000 |
Worksheet window (1 hour) |
GENERAL_RATE_LIMIT_MAX_REQUESTS |
Express | 100 |
Requests per IP per window for all /api/* |
GENERAL_RATE_LIMIT_WINDOW_MS |
Express | 900000 |
General window (15 minutes) |
ALLOWED_ORIGINS |
Cloudflare | - | Extra origins allowed to call the API (comma-separated); same-origin is always allowed |
RATE_LIMIT_KV (KV binding) |
Cloudflare | - | KV namespace used for per-IP rate limiting |
Request bodies are limited to 1 KB on both implementations.
- Create a Pages project from this repository. Build command
npm run build:client, output directoryserver/client/dist(already set inwrangler.toml). - Add
OPENAI_API_KEYas an encrypted environment variable for Production (and Preview if you want previews to work). - Create a KV namespace and bind it to the project as
RATE_LIMIT_KV(Settings → Bindings). - Recommended: add a Cloudflare WAF rate-limiting rule on
/api/*(e.g. 20 requests / 10 minutes per IP). KV counters are eventually consistent, so the WAF rule is the hard edge limit and the function-level limit is the per-user quota. - Set a monthly spend limit on the OpenAI API key you use for the deployment.
To run the Pages function locally:
npm run build
npx wrangler pages dev server/client/dist --kv RATE_LIMIT_KV --binding OPENAI_API_KEY=sk-...- Rate limiting: 10 worksheet generations per hour per IP (Express and Cloudflare), plus a general limit on the Express API and progressive slow-down after three worksheet requests.
- Input validation: shared Zod schema - K-12 grade levels only, topic 3-100 characters, inappropriate-content and spam filters, unknown fields rejected.
- Request size limits: 1 KB body limit before any parsing.
- Upstream error masking: OpenAI errors are never forwarded to the client.
- Security headers: Helmet on Express;
server/client/public/_headerson Cloudflare Pages (CSP, HSTS, frame denial, etc.). - Secrets: only
OPENAI_API_KEYis needed; keep it inserver/.env(git-ignored) or the Cloudflare dashboard, never in the repo. - CI: read-only
GITHUB_TOKEN, SHA-pinned actions, Dependabot for npm and GitHub Actions.
- Request caching to reduce API costs
- User accounts with per-user quotas and saved worksheets
- Additional worksheet formats and languages
- Fork the repository and create a feature branch
- Run
npm test,npm run lint --prefix serverandnpm run buildbefore opening a PR - Open a Pull Request against
main
MIT - see LICENSE.