Skip to content

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

TechnologyVersionPurpose
Express4.21HTTP framework
TypeScript5.7Type safety
Prisma6.3ORM for PostgreSQL
PostgreSQL17Database
Zod3.24Request validation + env parsing
jsonwebtoken9.0JWT signing and verification
bcryptjs2.4Password hashing (12 salt rounds)
helmet8.0Security headers
morgan1.10HTTP request logging
multer1.4File upload handling
cloudinary2.5CDN file uploads
uuid11.0UUID 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.example

Layer-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):

  1. helmet() -- security headers
  2. cors() -- CORS with configured origins
  3. morgan('dev') -- HTTP request logging
  4. express.json({ limit: '10mb' }) -- JSON body parsing
  5. express.urlencoded() -- form body parsing
  6. Routes -- all mounted at /api/portal
  7. 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

ServiceRoute PrefixAuthPrisma Models
Auth/authPublic + AuthAppUser, RefreshToken
Users/usersAuthAppUser, AppUserProfile, AppUserSettings
Challenges/v2/challengesOptional + AuthAppChallenge, AppCategory
User Challenges/v2/my-challengesAuthUserChallenge
Track Records/v2/track-recordsAuthTrackRecord, TrackRecordEntry
Gamification/v2/gamificationAuth + PublicXPEvent, Badge, UserBadge
Communities/v2/communitiesOptional + AuthIn-memory (planned)
Schools/v2/schoolsAuth + RoleIn-memory (planned)
Admin/v2/adminAuth + Role(admin)AppUser, AppChallenge
Explore/v2/exploreOptional + AuthAppChallenge
Vendor/v2/vendorAuth + Role(vendor)In-memory (planned)
Gifts/v2/giftsAuthIn-memory (planned)
Invitations/v2/invitationsAuthIn-memory (planned)
Learning Paths/v2/learning-pathsAuthIn-memory (planned)
Notifications/v2/notificationsAuthIn-memory (planned)
Portfolios/v2/portfoliosAuthIn-memory (planned)
Reflections/v2/reflectionsAuthIn-memory (planned)
Payments/v2/paymentsAuthIn-memory (planned)
Events/v2/eventsAuthIn-memory (planned)
Onboarding/v2/onboardingAuthIn-memory (planned)
Prod Data/ (root)PublicLegacy 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 TypeStatusBehavior
ZodError400Returns field-level validation messages
JsonWebTokenError401"Invalid token"
TokenExpiredError401"Token expired"
AppErrorvariesCustom status code + message
Unknown500Safe 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

DoCurious Platform Documentation