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: [email protected]
- 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(default- all)
- q— keyword
- scope—- name|email|all(default- all)
- mode—- contains|startsWith(default- contains)
- start— ISO timestamp (UTC) — filter by- createdAt ≥ start
- end— ISO timestamp (UTC) — filter by- createdAt ≤ end
- page— page number (default- 1)
- limit— items per page (default- 10, max- 50)
- sort—- field:dir(default- createdAt: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
- emailunique
- createdAtindex (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":"[email protected]","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":"[email protected]","password":"Password@123","role":"student"}'- npm run dev— start Fastify with tsx (watch mode)
- npm run build— compile TypeScript to- dist/
- npm run start— run compiled build
- npm 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)