Guide Développeur
Guide pratique pour développer sur la plateforme Roomee.
Installation et Configuration
Prérequis
bash
# Versions requises
node --version # v20+
pnpm --version # v8+
docker --version # v24+Installation Initiale
bash
# Cloner le repo
git clone https://github.com/roomee/services.git
cd services
# Installer les dépendances
pnpm install
# Configurer les variables d'environnement
cp .env.example .envConfiguration Locale
Fichier .env à la racine :
bash
# Base de données
DATABASE_URL="mongodb://localhost:27017/roomee"
# JWT
JWT_ACCESS_SECRET="your-secret-key-change-in-production"
PASSWORD_ENCRYPTION_KEY="32-char-encryption-key-here"
# AMQP
AMQP_GATEWAY_URL="amqp://localhost:5672"
# Services externes (optionnel en local)
SENDBIRD_ORGANIZATION_API_TOKEN="..."
STRIPE_SECRET_KEY="..."
ONESIGNAL_API_KEY="..."Démarrage des Services
Option 1 : Tous les services
bash
pnpm devOption 2 : Service spécifique
bash
cd services/api-authentication
npm run devOption 3 : Avec Docker Compose
bash
docker-compose up -dCommandes Essentielles
Monorepo (racine)
bash
# Développement
pnpm dev # Tous les services
pnpm dev --filter=api-authentication # Service spécifique
# Build
pnpm build # Tous les services
pnpm build --filter=api-news # Service spécifique
# Tests
pnpm test # Tous les tests
pnpm test --filter=api-hotel # Tests d'un service
# Lint
pnpm lint # Tous les services
pnpm lint:fix # Auto-fix
# Clean
pnpm clean # Nettoie node_modules et dist/Par Service
bash
cd services/api-[service-name]
# Développement avec hot reload
npm run dev
# Build
npm run build
# Tests
npm run test
npm run test:watch
npm run test:coverage
# Prisma
npm run prisma:generate # Génère le client après changement schéma
npm run prisma:migrate # Crée une migration
npm run prisma:push # Push schéma sans migration (dev only)
npm run prisma:studio # UI Prisma
# Documentation
npm run swagger-autogen # Génère la doc SwaggerStructure d'un Service
Service Moderne (TypeScript)
services/api-news/
├── src/
│ ├── app.ts # Config Express (Singleton)
│ ├── server.ts # Point d'entrée
│ ├── config/
│ │ └── config.ts # ConfigService
│ ├── modules/ # Organisation modulaire
│ │ └── page/
│ │ ├── controllers/
│ │ ├── services/
│ │ ├── routes/
│ │ ├── validators/
│ │ └── types/
│ ├── middlewares/
│ ├── utils/
│ └── types/
├── prisma/
│ └── schema.prisma
├── dist/ # Code compilé
├── tsconfig.json
└── package.jsonService Legacy (JavaScript)
services/api-hotel/
├── app.js # Config Express
├── loader.js # Point d'entrée
├── controllers/
├── routes/
├── repository/
├── middlewares/
├── socket/
├── amqp/
├── config/
├── utils/
├── prisma/
│ └── schema.prisma
└── package.jsonCréer un Nouveau Service
1. Initialiser le Service
bash
# Créer le dossier
mkdir -p services/api-my-service
cd services/api-my-service
# Initialiser npm
npm init -y
# Installer les dépendances
npm install express prisma @prisma/client
npm install -D typescript @types/node @types/express ts-node nodemon
# Ajouter @roomee/shared
npm install @roomee/shared@workspace:*2. Configuration TypeScript
tsconfig.json :
json
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}3. Initialiser Prisma
bash
npx prisma init --datasource-provider mongodbprisma/schema.prisma :
prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
model MyModel {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
created_at DateTime @default(now())
updated_at DateTime @updatedAt
deleted_at DateTime?
@@index([created_at])
}4. Structure de Base
src/server.ts :
typescript
import { App } from './app'
import { configService } from './config/config'
async function startServer() {
const config = await configService.configureEnvVar()
const app = App.getInstance()
await app.init()
const server = app.getApp().listen(config.port, () => {
console.log(`Server running on port ${config.port}`)
})
// Graceful shutdown
process.on('SIGTERM', () => {
server.close(() => process.exit(0))
})
}
startServer()src/app.ts :
typescript
import express, { Express } from 'express'
import helmet from 'helmet'
import cors from 'cors'
import { authMiddleware, sentryService, errorHandler } from '@roomee/shared'
export class App {
private static instance: App
private readonly app: Express
private constructor() {
this.app = express()
}
public static getInstance(): App {
if (!App.instance) {
App.instance = new App()
}
return App.instance
}
public async init(): Promise<void> {
// Sentry
sentryService.initialize(config.sentry)
sentryService.setupExpressHandlers(this.app)
// Middlewares
this.app.use(helmet())
this.app.use(cors())
this.app.use(express.json())
// Auth
authMiddleware.initialize({ jwtAccessSecret: config.jwtAccessSecret })
// Routes
this.setupRoutes()
// Error handling
this.app.use(errorHandler)
}
private setupRoutes(): void {
this.app.get('/health', (req, res) => {
res.json({ status: 'UP' })
})
// Import et monter les routes
}
public getApp(): Express {
return this.app
}
}Patterns de Développement
Repository Pattern
typescript
// repository/myModel.repository.ts
import { prisma } from '@/config/prisma'
export class MyModelRepository {
async findById(id: string) {
return await prisma.myModel.findUnique({
where: { id, deleted_at: null }
})
}
async findAll(workspaceId: string, filters: any) {
return await prisma.myModel.findMany({
where: {
workspaceId,
deleted_at: null,
...filters
}
})
}
async create(data: any) {
return await prisma.myModel.create({ data })
}
async update(id: string, data: any) {
return await prisma.myModel.update({
where: { id },
data: { ...data, updated_at: new Date() }
})
}
async softDelete(id: string, deletedById: string) {
return await prisma.myModel.update({
where: { id },
data: { deleted_at: new Date(), deletedById }
})
}
}Service Pattern
typescript
// services/myModel.service.ts
import { MyModelRepository } from '@/repository'
import { NotFoundError } from '@roomee/shared'
export class MyModelService {
constructor(private repository: MyModelRepository) {}
async getById(id: string) {
const model = await this.repository.findById(id)
if (!model) {
throw new NotFoundError('Model not found')
}
return model
}
async create(data: CreateDto) {
// Validation métier
await this.validateData(data)
// Création
const model = await this.repository.create(data)
// Événement AMQP
await this.emitCreatedEvent(model)
return model
}
private async validateData(data: any) {
// Logique de validation
}
private async emitCreatedEvent(model: any) {
await amqpServer.emit('model.created', model)
}
}Controller Pattern
typescript
// controllers/myModel.controller.ts
import { Request, Response } from 'express'
import { MyModelService } from '@/services'
import { sendSuccess, sendError } from '@roomee/shared'
export class MyModelController {
constructor(private service: MyModelService) {}
getById = async (req: Request, res: Response) => {
try {
const model = await this.service.getById(req.params.id)
sendSuccess(res, model)
} catch (error) {
sendError(res, error)
}
}
create = async (req: Request, res: Response) => {
try {
const model = await this.service.create(req.body)
sendSuccess(res, model, 201)
} catch (error) {
sendError(res, error)
}
}
}Route Pattern
typescript
// routes/myModel.routes.ts
import { Router } from 'express'
import { authMiddleware } from '@roomee/shared'
import { validate } from '@/middlewares'
import { myModelController } from '@/controllers'
import { createMyModelSchema } from '@/validators'
const router = Router()
// Toutes les routes nécessitent auth
router.use(authMiddleware.middleware)
router.get('/:id', myModelController.getById)
router.post('/', validate(createMyModelSchema), myModelController.create)
export default routerValidation avec Zod
Créer un Schema
typescript
// validators/myModel.validator.ts
import { z } from 'zod'
export const createMyModelSchema = z.object({
body: z.object({
name: z.string().min(1).max(255),
workspaceId: z.string().regex(/^[0-9a-fA-F]{24}$/),
status: z.enum(['active', 'inactive']).optional()
})
})
export const updateMyModelSchema = z.object({
params: z.object({
id: z.string().regex(/^[0-9a-fA-F]{24}$/)
}),
body: z.object({
name: z.string().min(1).max(255).optional(),
status: z.enum(['active', 'inactive']).optional()
})
})
export type CreateMyModelDto = z.infer<typeof createMyModelSchema>['body']
export type UpdateMyModelDto = z.infer<typeof updateMyModelSchema>['body']Middleware de Validation
typescript
// middlewares/validate.middleware.ts
import { Request, Response, NextFunction } from 'express'
import { AnyZodObject } from 'zod'
import { ValidationError } from '@roomee/shared'
export const validate = (schema: AnyZodObject) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
await schema.parseAsync({
body: req.body,
query: req.query,
params: req.params
})
next()
} catch (error) {
next(new ValidationError('Validation failed', error.errors))
}
}
}Tests
Tests Unitaires (Jest)
typescript
// __tests__/services/myModel.service.test.ts
import { MyModelService } from '@/services'
import { MyModelRepository } from '@/repository'
describe('MyModelService', () => {
let service: MyModelService
let repository: MyModelRepository
beforeEach(() => {
repository = new MyModelRepository()
service = new MyModelService(repository)
})
describe('getById', () => {
it('should return model when found', async () => {
const mockModel = { id: '123', name: 'Test' }
jest.spyOn(repository, 'findById').mockResolvedValue(mockModel)
const result = await service.getById('123')
expect(result).toEqual(mockModel)
expect(repository.findById).toHaveBeenCalledWith('123')
})
it('should throw NotFoundError when not found', async () => {
jest.spyOn(repository, 'findById').mockResolvedValue(null)
await expect(service.getById('123')).rejects.toThrow('Model not found')
})
})
})Tests d'Intégration (Supertest)
typescript
// __tests__/routes/myModel.routes.test.ts
import request from 'supertest'
import { App } from '@/app'
describe('MyModel Routes', () => {
let app: Express
let authToken: string
beforeAll(async () => {
app = App.getInstance().getApp()
// Login pour obtenir token
const response = await request(app)
.post('/auth/login')
.send({ email: 'test@test.com', password: 'password' })
authToken = response.body.accessToken
})
describe('GET /:id', () => {
it('should return model by id', async () => {
const response = await request(app)
.get('/mymodels/123')
.set('Authorization', `Bearer ${authToken}`)
expect(response.status).toBe(200)
expect(response.body.data).toHaveProperty('id', '123')
})
it('should return 401 without auth', async () => {
const response = await request(app).get('/mymodels/123')
expect(response.status).toBe(401)
})
})
})Debugging
VS Code Launch Configuration
.vscode/launch.json :
json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug api-authentication",
"runtimeArgs": [
"-r",
"ts-node/register",
"--inspect-brk"
],
"args": ["${workspaceFolder}/services/api-authentication/src/server.ts"],
"env": {
"NODE_ENV": "development"
},
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"sourceMaps": true,
"restart": true
}
]
}Logs Winston
typescript
import { logger } from '@roomee/shared'
// Niveaux: error, warn, info, debug
logger.info('User logged in', { userId, email })
logger.error('Database error', { error, query })
logger.debug('Processing request', { body: req.body })Prisma Studio
bash
cd services/api-[service]
npx prisma studioOuvre une UI sur http://localhost:5555 pour explorer la base de données.
Git Workflow
Branches
main # Production
├── staging # Pré-production
└── develop # Développement
└── feature/[nom-feature]Commits
Convention : Conventional Commits
bash
feat: add user profile endpoint
fix: correct pagination bug in news service
docs: update API documentation
refactor: extract repository pattern
test: add unit tests for auth service
chore: update dependenciesPull Request
bash
# Créer une branche
git checkout -b feature/add-notifications
# Développer et committer
git add .
git commit -m "feat: add notification preferences"
# Push
git push origin feature/add-notifications
# Créer la PR sur GitHubBonnes Pratiques
1. Toujours Filtrer par Workspace
typescript
// ✅ BON
const data = await prisma.model.findMany({
where: {
workspaceId: req.user.workspaceId,
deleted_at: null
}
})2. Toujours Soft Delete
typescript
// ✅ BON
await prisma.model.update({
where: { id },
data: { deleted_at: new Date(), deletedById }
})3. Valider les Entrées
typescript
// ✅ BON
router.post('/', validate(createSchema), controller.create)4. Logger les Actions Importantes
typescript
logger.info('Member created', { memberId, workspaceId })
logger.error('Failed to send notification', { error, notificationId })5. Gérer les Erreurs Proprement
typescript
try {
await service.create(data)
} catch (error) {
if (error instanceof ValidationError) {
return res.status(400).json({ error: error.message })
}
throw error // Re-throw pour le error handler
}Ressources Utiles
Documentation
Outils
- Postman : Tester les API
- MongoDB Compass : Explorer MongoDB
- RabbitMQ Management : http://localhost:15672
- Prisma Studio : UI pour la DB
Support
- Slack : #backend-dev
- Documentation interne : https://docs.roomee.io
- Wiki : https://wiki.roomee.io