You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
# Online Exam API
Modern Node.js + Express REST API powering the Online Exam Portal.
This is a clean rewrite of the original college-era project: layered (routes -> controllers -> services -> repositories), security-hardened, fully tested, and ready to swap the in-memory store for a real database.
---
## Highlights
- Layered architecture: `routes -> controllers -> services -> repositories`
- Pluggable storage layer: `memory`, `postgres`, `mssql` (selected via `REPOSITORY_DRIVER`)
- JWT auth with **bcryptjs**-hashed passwords
- Role-based access control (`student`, `admin`)
- Per-route validation with `express-validator`
- Tiered rate limiting (general, auth, exam)
- Helmet + strict CORS allow-list
- Centralised `ApiError` and JSON error envelope
- Full unit + integration test suite (`jest` + `supertest`) - 60+ tests
- `AUTH_BYPASS=true` switch for local dev/demos (refused in production)
- Configurable via `.env`, validated at boot
- Graceful shutdown on `SIGINT` / `SIGTERM` (closes DB pools)
---
## Project structure
```
online-exam-api/
|-- src/
| |-- app.js Express app factory
| |-- config/
| | |-- env.js Validated env config
| | `-- logger.js Tiny levelled logger
| |-- middleware/
| | |-- asyncHandler.js
| | |-- auth.js authenticate + requireAdmin
| | |-- errorHandler.js
| | |-- notFound.js
| | |-- rateLimiter.js
| | |-- security.js Helmet + CORS
| | `-- validate.js
| |-- controllers/ Thin HTTP handlers
| |-- services/ Business logic
| |-- repositories/ Data layer (in-memory, swappable)
| |-- routes/ Express routers
| |-- validators/ Request schemas
| `-- utils/
| |-- ApiError.js
| `-- seed.js Seeds default users + demo exam
|-- tests/
| |-- unit/
| | |-- middleware/
| | `-- services/
| |-- integration/
| | `-- routes/
| |-- helpers.js
| `-- setupEnv.js
|-- server.js Process entry
|-- jest.config.js
|-- .eslintrc.json / .prettierrc.json
|-- .env.example
`-- package.json
```
---
## Getting started
```bash
npm install
cp .env.example .env
npm run dev # auto-reload with nodemon
# or
npm start
```
The server boots on `http://localhost:8081` by default and seeds:
| Role | Email | Password |
|---------|------------------------|-----------------|
| admin | admin@example.com | Admin@12345 |
| student | student@example.com | Student@12345 |
It also seeds one demo exam with five JavaScript questions so the UI works out of the box.
> Override seed credentials via `SEED_ADMIN_EMAIL`, `SEED_ADMIN_PASSWORD`, etc.
---
## Scripts
| Command | Description |
|-------------------------|--------------------------------------------|
| `npm start` | Start the server |
| `npm run dev` | Start with nodemon (auto-reload) |
| `npm test` | Run the full Jest test suite |
| `npm run test:coverage` | Run tests with a coverage report |
| `npm run lint` | Run ESLint |
| `npm run lint:fix` | ESLint with `--fix` |
| `npm run format` | Prettier on the project |
---
## Endpoints
All non-auth routes live under `/api`. JSON envelopes look like:
```json
{ "success": true, "exam": { } }
```
```json
{ "success": false, "error": { "status": 400, "message": "...", "details": [] } }
```
### Auth (`/api/auth`)
| Method | Path | Auth | Body / Notes |
|--------|-------------|----------|-----------------------------------------------|
| POST | `/register` | public | `{ email, password (>=8), name }` |
| POST | `/login` | public | `{ email, password }` -> `{ token, user }` |
| POST | `/refresh` | bearer | issues a new token |
| GET | `/me` | bearer | returns the current user |
### Exams (`/api/exams`)
| Method | Path | Auth |
|--------|----------|---------------|
| GET | `/` | any user |
| GET | `/:id` | any user |
| POST | `/` | admin |
| PUT | `/:id` | admin |
| DELETE | `/:id` | admin |
`correctAnswerIndex` is hidden from non-admin viewers.
### Questions (`/api/questions`)
| Method | Path | Auth |
|--------|---------------------|--------|
| GET | `/exam/:examId` | any |
| GET | `/:id` | any |
| POST | `/` | admin |
| PUT | `/:id` | admin |
| DELETE | `/:id` | admin |
### Submissions (`/api/submissions`)
| Method | Path | Auth |
|--------|-----------------------|---------------|
| POST | `/exam/:examId` | bearer |
| GET | `/me` | bearer |
| GET | `/:id` | owner / admin |
| GET | `/user/:userId` | admin |
### Health
`GET /api/health` -> `{ success: true, status: "ok", uptime, timestamp }`
---
## Security notes
- Passwords stored as bcrypt hashes; constant-time-ish login defence vs. timing oracles.
- `JWT_SECRET` must be set in production (boot fails otherwise).
- `helmet` with strict CSP, HSTS, frameguard, no-sniff.
- CORS uses an explicit allow-list (set via `CORS_ORIGIN`).
- Tiered rate limits on `/api/auth/*` (10/15min) and `/api/submissions/exam/*` (30/min).
- Body size capped at 50 KB by default.
- `correctAnswerIndex` is stripped from responses for non-admin users.
---
## Storage drivers
The data layer is dispatched at boot by `REPOSITORY_DRIVER`:
| Driver | Backed by | Notes |
|------------|------------------------------------|-------------------------------------------------|
| `memory` | In-process `Map` (default) | Resets on restart. Used by all tests. |
| `postgres` | `pg` connection pool | Runs migrations from `db/migrations/postgres.sql` |
| `mssql` | `mssql` (tedious) connection pool | Runs migrations from `db/migrations/mssql.sql` |
`pg` and `mssql` are listed under `optionalDependencies`, so they install with the project but the modules are only required at boot time when the matching driver is selected. Switching drivers is purely env-driven - no code changes:
```bash
# Postgres
REPOSITORY_DRIVER=postgres DATABASE_URL=postgres://user:pass@host:5432/online_exam npm start
# MSSQL
REPOSITORY_DRIVER=mssql MSSQL_HOST=host MSSQL_USER=sa MSSQL_PASSWORD=... MSSQL_DATABASE=online_exam npm start
# Default
REPOSITORY_DRIVER=memory npm start
```
### Running migrations
```bash
# Postgres
psql "$DATABASE_URL" -f db/migrations/postgres.sql
# or:
npm run db:postgres
# MSSQL (requires sqlcmd on PATH)
npm run db:mssql
```
### Disabling the seed step
When pointing the API at a real production database you usually do not want it
to seed demo users / a demo exam every boot. Set `SEED_ON_BOOT=false` to skip.
### Repository interface
Every driver exposes the same shape from `src/repositories/index.js`:
```
users { findByEmail, findById, create, list, reset }
exams { findById, list, create, update, delete, reset }
questions { findById, findByExam, create, update, delete, deleteByExam, reset }
submissions { findById, findByUser, findByExamAndUser, create, reset }
```
All SQL queries are parameterized; no string concatenation anywhere.
### Auth bypass (development only)
`AUTH_BYPASS=true` short-circuits auth entirely - every protected route is
treated as authenticated as a fixed bypass user (admin by default). The server
refuses to start with `AUTH_BYPASS=true` when `NODE_ENV=production`.
---
## License
MIT
About
Rest api for online exam portal backend using node.js and sql. Used jwt for authentication