Fast, typed API for Classboard using Fastify + TypeScript + MongoDB (Mongoose) with JWT auth and structured logging.
- Auth: register, login, current user (
/me) - Users: list with filters (role, date range, search), CRUD, bulk disable/role
- Suggestions: quick global search (
/users/suggestions) - Metrics: dashboard summary + signups time series
- Roles:
admin/teacher/studentenforced on the server - Pino logs & health probe (
/health)
- Runtime: Node.js 20+
- Framework: Fastify
- Language: TypeScript
- DB: MongoDB 6+/8+ (Mongoose v8)
- Validation: Zod
- Auth: JWT (HTTP
Authorization: Bearer <token>)
Frontend (Next.js) uses an HTTP‑only cookie to store the JWT and simply forwards it to this API.
src/
models/ # Mongoose schemas (User, ...)
routes/ # Fastify route files (auth, users, metrics)
utils/ # authGuard, dates, password helpers, etc.
server.ts # Fastify bootstrap
- Node 20+
- MongoDB running locally (default URL:
mongodb://localhost:27017)
npm iCreate .env in the project root (see also .env.example):
MONGO_URL=mongodb://localhost:27017/classboard
JWT_SECRET=change-me-to-a-long-random-string
PORT=4000
CORS_ORIGIN=http://localhost:3000
BCRYPT_SALT_ROUNDS=10npm run devIf it works you should see logs like:
MongoDB connected
Server running on http://localhost:4000
npm run seedAdmin credentials (after seeding):
- Email:
admin@classboard.local - Password:
Admin@123
If you don’t seed, register any user and promote their
roleto"admin"in MongoDB Compass: DBclassboard→ Collectionusers→ edit document.
- Client logs in via
POST /auth/loginand receives a JWT{ token }. - Subsequent requests include a header:
Authorization: Bearer <token>. GET /mereturns the current user.
The Next.js frontend stores the JWT in an HTTP‑only cookie and calls this API through its own
/api/*route handlers.
All endpoints are prefixed from the server root (e.g., http://localhost:4000).
GET /health→{ ok: true }
-
POST /auth/register- Body:
{ name: string, email: string, password: string } - Notes: server assigns
role = "student"unless you add a whitelist/invite feature. - Returns:
{ token, user }
- Body:
-
POST /auth/login- Body:
{ email: string, password: string } - Returns:
{ token, user }
- Body:
-
GET /me- Header:
Authorization: Bearer <token> - Returns: user JSON (sans passwordHash)
- Header:
-
GET /users-
Query params:
role—admin|teacher|student|all(defaultall)q— keywordscope—name|email|all(defaultall)mode—contains|startsWith(defaultcontains)start— ISO timestamp (UTC) — filter bycreatedAt ≥ startend— ISO timestamp (UTC) — filter bycreatedAt ≤ endpage— page number (default1)limit— items per page (default10, max50)sort—field:dir(defaultcreatedAt:desc)
-
Returns:
{ data: User[], page: number, total: number }
-
-
GET /users/:id- Returns the user by id (no password hash)
-
POST /users(admin only)- Body:
{ name, email, password, role, bio?, avatarUrl? } - Creates a new user
- Body:
-
PATCH /users/:id(admin or self‑limited)- Admin may update any allowed field.
- Non‑admin may only update:
name,bio,avatarUrl,preferences.
-
DELETE /users/:id(admin only) -
PATCH /users/bulk(admin only)-
Body examples:
{ ids: ["..."], disabled: true }{ ids: ["..."], role: "teacher" }
-
Returns:
{ ids, matched, modified }
-
-
GET /users/suggestions- Query:
q,limit(default8) - Returns simple array of
{ id, name, email, role }
- Query:
-
GET /metrics/summary- Returns totals & deltas for dashboard KPI cards
-
GET /metrics/signups?start=...&end=...&interval=day- Returns:
[{ date: "yyyy/mm/dd", count: number }, ...] - Date bucketing is UTC.
- Returns:
{
_id: ObjectId,
name: string,
email: string, // unique, lower‑cased
role: "admin" | "teacher" | "student",
bio?: string,
avatarUrl?: string,
disabled?: boolean,
preferences?: {
theme?: "system" | "light" | "dark",
density?: "comfortable" | "compact",
language?: string
},
createdAt: ISOString,
updatedAt: ISOString,
passwordHash: string // stored only in DB, never returned
}Indexes
emailuniquecreatedAtindex (for sort/range queries)- Optional:
roleindex
# 1) Login (admin)
curl -s -X POST http://localhost:4000/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@classboard.local","password":"Admin@123"}'
# 2) List users whose *name starts with* "a"
curl -s "http://localhost:4000/users?q=a&scope=name&mode=startsWith&role=all" \
-H "Authorization: Bearer <TOKEN>"
# 3) Create a user (admin only)
curl -s -X POST http://localhost:4000/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <TOKEN>" \
-d '{"name":"Alice","email":"alice@example.com","password":"Password@123","role":"student"}'npm run dev— start Fastify with tsx (watch mode)npm run build— compile TypeScript todist/npm run start— run compiled buildnpm run seed— create default admin user
- Use a long random
JWT_SECRETin production - Keep HTTPS at the proxy; restrict CORS with
CORS_ORIGIN - All admin actions validated server‑side (never trust client role)
- Passwords stored as
bcrypthashes
- 401 Unauthorized on
/meor/users: missing/expired token → login again - CORS error from browser: set
CORS_ORIGIN=http://localhost:3000(or your frontend origin) - Mongo connection fails: check
MONGO_URLand ensuremongodis running - 409 Email already in use: the email exists; use a different one
- Build:
npm run build - Provide production
.env - Start:
npm run start(or run under PM2/systemd) - Put a reverse proxy (Nginx/Caddy) in front with HTTPS. Allow only your frontend origin via CORS.
- Email whitelist / invites to auto‑assign roles on signup
- Audit log for admin actions
- Rate‑limit & IP throttling
- E2E & unit tests (Vitest)
MIT (or your preference)