Backend Architecture
The YeboJobs backend is a Node.js/Express API built with TypeScript, using Prisma as the ORM for PostgreSQL.
Tech Stack
- Runtime: Node.js 20+
- Framework: Express.js with TypeScript
- Database: PostgreSQL (Neon Serverless)
- ORM: Prisma
- Authentication: JWT (access + refresh tokens)
- Validation: Joi
- Payments: Stripe
Project Structure
backend/
├── prisma/
│ └── schema.prisma # Database schema
├── src/
│ ├── app.ts # Express app configuration
│ ├── config/
│ │ └── prisma.ts # Prisma client singleton
│ ├── routes/
│ │ ├── index.ts # Route aggregator
│ │ ├── auth.routes.ts # Authentication routes
│ │ ├── job.routes.ts # Job CRUD routes
│ │ ├── user.routes.ts # User profile routes
│ │ └── ... # Other route files
│ ├── controllers/
│ │ ├── auth.controller.ts
│ │ ├── job.controller.ts
│ │ └── ...
│ ├── services/
│ │ ├── auth.service.ts
│ │ ├── job.service.ts
│ │ └── ...
│ ├── middleware/
│ │ ├── auth.middleware.ts
│ │ └── validation.middleware.ts
│ └── utils/
│ ├── jwt.ts
│ ├── ApiResponse.ts
│ └── currencies.ts
└── package.jsonApplication Setup
The main app.ts configures Express with:
typescript
import express from 'express';
import cors from 'cors';
import routes from '@routes/index';
const app = express();
app.use(cors({
origin: [
'http://localhost:5173',
'https://yebojobs.pages.dev',
'https://yebojobs.com'
],
credentials: true,
}));
app.use(express.json({ limit: '10mb' }));
app.use('/api', routes);
export default app;API Route Structure
All routes are prefixed with /api:
| Route Group | Base Path | Description |
|---|---|---|
| Auth | /api/auth | Authentication & registration |
| Jobs | /api/jobs | Job listings CRUD |
| Users | /api/users | User profiles |
| Applications | /api/applications | Job applications |
| Employers | /api/employers | Employer profiles |
| Services | /api/services | Service worker profiles |
| Bookings | /api/bookings | Service bookings |
| Messages | /api/messages | Messaging system |
| Credits | /api/credits | Credit wallet system |
| Billing | /api/billing | Stripe subscription plans |
| Interviews | /api/interviews | AI interview integration |
| Experience | /api/experience | Experience Lab tracks |
Key Design Patterns
Service Layer Pattern
All business logic is in services. Controllers are thin - they validate input, call services, and format responses:
typescript
// Controller
static async getJob(req: Request, res: Response) {
const job = await JobService.getJobById(req.params.id);
if (!job) return ApiResponse.notFound(res, 'Job not found');
ApiResponse.success(res, job);
}
// Service
static async getJobById(jobId: string) {
return prisma.job.findUnique({
where: { id: jobId },
include: { employer: true }
});
}Standardized API Responses
All responses use the ApiResponse utility:
typescript
class ApiResponse {
static success(res, data, message = 'Success') {
return res.json({ success: true, data, message });
}
static notFound(res, message) {
return res.status(404).json({ success: false, message });
}
// ... etc
}Token-based Authentication
JWT with access (15m) + refresh (7d) tokens:
typescript
// Generate tokens
const accessToken = JWTUtil.generateAccessToken({ id, type: 'user' });
const refreshToken = JWTUtil.generateRefreshToken({ id, type: 'user' });
// Verify in middleware
const decoded = JWTUtil.verifyAccessToken(token);
req.user = decoded;Environment Variables
bash
DATABASE_URL=postgresql://...
DIRECT_URL=postgresql://...
JWT_SECRET=your-secret
JWT_REFRESH_SECRET=your-refresh-secret
STRIPE_SECRET_KEY=sk_...
OKIA_API_URL=https://okia-service-...
OKIA_API_KEY=...
FRONTEND_URL=https://yebojobs.pages.devDatabase Connection
Prisma client is a singleton:
typescript
// config/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}Error Handling
Controllers wrap operations in try-catch:
typescript
try {
const result = await SomeService.doSomething();
ApiResponse.success(res, result);
} catch (error: any) {
ApiResponse.serverError(res, error.message, error);
}