Appearance
Backend Architecture
The DoCurious backend is an Express server that provides a REST API for the frontend. It follows a layered architecture with clear separation between routing, business logic, and data access.
STATUS: BUILT
The server has 130+ endpoints across 18 domain services, JWT authentication, role-based access control, and Zod request validation.
Tech Stack
| Technology | Version | Purpose |
|---|---|---|
| Express | 4.21 | HTTP framework |
| TypeScript | 5.7 | Type safety |
| Prisma | 6.3 | ORM for PostgreSQL |
| PostgreSQL | 17 | Database |
| Zod | 3.24 | Request validation + env parsing |
| jsonwebtoken | 9.0 | JWT signing and verification |
| bcryptjs | 2.4 | Password hashing (12 salt rounds) |
| helmet | 8.0 | Security headers |
| morgan | 1.10 | HTTP request logging |
| multer | 1.4 | File upload handling |
| cloudinary | 2.5 | CDN file uploads |
| uuid | 11.0 | UUID generation |
Architecture Diagram
Project Structure
server/
├── prisma/
│ ├── schema.prisma Prisma schema (legacy + modern models)
│ └── seed.ts Database seeding script
├── src/
│ ├── index.ts Entry point (listens on PORT)
│ ├── app.ts Express app factory
│ ├── config/
│ │ ├── env.ts Zod-validated environment variables
│ │ └── cors.ts CORS origin allowlist
│ ├── lib/
│ │ └── prisma.ts Prisma client singleton
│ ├── middleware/
│ │ ├── auth.ts JWT authentication (requireAuth, optionalAuth)
│ │ ├── requireRole.ts Role-based access control
│ │ ├── validate.ts Zod request body/query validation
│ │ └── errorHandler.ts Global error handler
│ ├── utils/
│ │ ├── jwt.ts JWT sign/verify helpers
│ │ ├── password.ts bcrypt hash/compare
│ │ ├── response.ts Standard response formatters
│ │ └── pagination.ts Pagination helpers
│ ├── types/
│ │ └── express.d.ts Express Request augmentation
│ ├── routes/ 22 route files
│ ├── controllers/ 19 controller files
│ ├── services/ 21 service files
│ └── __tests__/ 25 test files
├── package.json
├── tsconfig.json
└── .env.exampleLayer-by-Layer
Config
Environment variables are parsed and validated with Zod at startup (src/config/env.ts). If any required variable is missing or invalid, the server exits immediately with a descriptive error.
CORS is configured from CORS_ORIGIN (comma-separated origins) in src/config/cors.ts.
Middleware Stack
The Express app applies middleware in this order (src/app.ts):
- helmet() -- security headers
- cors() -- CORS with configured origins
- morgan('dev') -- HTTP request logging
- express.json({ limit: '10mb' }) -- JSON body parsing
- express.urlencoded() -- form body parsing
- Routes -- all mounted at
/api/portal - errorHandler -- global error catch (must be last)
Authentication Middleware
Two auth middleware functions (src/middleware/auth.ts):
typescript
// Mandatory — returns 401 if no valid token
requireAuth(req, res, next)
// Optional — attaches user if token present, continues regardless
optionalAuth(req, res, next)Both extract the JWT from Authorization: Bearer <token>, verify it, and attach req.user = { userId, role }.
Role Guard
typescript
// Restrict to specific roles — returns 403 if role doesn't match
requireRole('platform_admin', 'school_admin')Must be chained after requireAuth. Checks req.user.role against the allowed list.
Validation
typescript
// Validate request body against a Zod schema
validate(registerSchema)
// Validate query parameters
validateQuery(paginationSchema)Returns 400 with field-level error messages on validation failure. Replaces req.body/req.query with the parsed (coerced and stripped) values.
Routes → Controllers → Services
All 18 domains follow the same pattern:
Routes define HTTP method, path, middleware chain, and call the controller:
typescript
// src/routes/challenges.routes.ts
router.get('/', optionalAuth, challengeController.listChallenges)
router.post('/', requireAuth, validate(createChallengeSchema), challengeController.create)Controllers handle HTTP concerns (req/res), delegate to services, and format responses:
typescript
// src/controllers/challenge.controller.ts
async listChallenges(req, res) {
try {
const result = await challengeService.list(req.query, req.user?.userId)
return paginated(res, result.data, result.pagination)
} catch (err) {
if (err instanceof AppError) return error(res, err.message, err.statusCode)
throw err
}
}Services contain business logic and Prisma queries:
typescript
// src/services/challenge.service.ts
async list(query, userId?) {
const { skip, take } = toPrismaArgs(query)
const [data, total] = await Promise.all([
prisma.appChallenge.findMany({ skip, take, orderBy: { createdAt: 'desc' } }),
prisma.appChallenge.count(),
])
return { data, pagination: buildPagination(total, query) }
}Domain Services
| Service | Route Prefix | Auth | Prisma Models |
|---|---|---|---|
| Auth | /auth | Public + Auth | AppUser, RefreshToken |
| Users | /users | Auth | AppUser, AppUserProfile, AppUserSettings |
| Challenges | /v2/challenges | Optional + Auth | AppChallenge, AppCategory |
| User Challenges | /v2/my-challenges | Auth | UserChallenge |
| Track Records | /v2/track-records | Auth | TrackRecord, TrackRecordEntry |
| Gamification | /v2/gamification | Auth + Public | XPEvent, Badge, UserBadge |
| Communities | /v2/communities | Optional + Auth | In-memory (planned) |
| Schools | /v2/schools | Auth + Role | In-memory (planned) |
| Admin | /v2/admin | Auth + Role(admin) | AppUser, AppChallenge |
| Explore | /v2/explore | Optional + Auth | AppChallenge |
| Vendor | /v2/vendor | Auth + Role(vendor) | In-memory (planned) |
| Gifts | /v2/gifts | Auth | In-memory (planned) |
| Invitations | /v2/invitations | Auth | In-memory (planned) |
| Learning Paths | /v2/learning-paths | Auth | In-memory (planned) |
| Notifications | /v2/notifications | Auth | In-memory (planned) |
| Portfolios | /v2/portfolios | Auth | In-memory (planned) |
| Reflections | /v2/reflections | Auth | In-memory (planned) |
| Payments | /v2/payments | Auth | In-memory (planned) |
| Events | /v2/events | Auth | In-memory (planned) |
| Onboarding | /v2/onboarding | Auth | In-memory (planned) |
| Prod Data | / (root) | Public | Legacy models (read-only) |
Authentication Flow
Response Format
All endpoints return a consistent JSON structure (src/utils/response.ts):
Success:
json
{ "success": true, "data": { ... } }Paginated:
json
{
"success": true,
"data": [ ... ],
"pagination": {
"page": 1,
"limit": 20,
"total": 142,
"totalPages": 8,
"hasMore": true
}
}Error:
json
{ "success": false, "error": "Descriptive error message" }Error Handling
The global error handler (src/middleware/errorHandler.ts) catches all unhandled errors:
| Error Type | Status | Behavior |
|---|---|---|
ZodError | 400 | Returns field-level validation messages |
JsonWebTokenError | 401 | "Invalid token" |
TokenExpiredError | 401 | "Token expired" |
AppError | varies | Custom status code + message |
| Unknown | 500 | Safe message in production, full error in development |
Services throw AppError for domain errors:
typescript
throw new AppError('Challenge not found', 404)
throw new AppError('Email already registered', 409)
throw new AppError('Insufficient permissions', 403)See Also
- Backend Quick Start -- get the server running
- Database & Prisma -- schema and model details
- API Endpoints -- complete endpoint reference
- Backend Testing -- test patterns
- API Layer -- frontend mock/real switching
- Adapter Layer -- FE-side data transformation