639 lines
16 KiB
Markdown
639 lines
16 KiB
Markdown
# Complete Examples - Full Working Code
|
|
|
|
Real-world examples showing complete implementation patterns.
|
|
|
|
## Table of Contents
|
|
|
|
- [Complete Controller Example](#complete-controller-example)
|
|
- [Complete Service with DI](#complete-service-with-di)
|
|
- [Complete Route File](#complete-route-file)
|
|
- [Complete Repository](#complete-repository)
|
|
- [Refactoring Example: Bad to Good](#refactoring-example-bad-to-good)
|
|
- [End-to-End Feature Example](#end-to-end-feature-example)
|
|
|
|
---
|
|
|
|
## Complete Controller Example
|
|
|
|
### UserController (Following All Best Practices)
|
|
|
|
```typescript
|
|
// controllers/UserController.ts
|
|
import { Request, Response } from 'express';
|
|
import { BaseController } from './BaseController';
|
|
import { UserService } from '../services/userService';
|
|
import { createUserSchema, updateUserSchema } from '../validators/userSchemas';
|
|
import { z } from 'zod';
|
|
|
|
export class UserController extends BaseController {
|
|
private userService: UserService;
|
|
|
|
constructor() {
|
|
super();
|
|
this.userService = new UserService();
|
|
}
|
|
|
|
async getUser(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
this.addBreadcrumb('Fetching user', 'user_controller', {
|
|
userId: req.params.id,
|
|
});
|
|
|
|
const user = await this.withTransaction(
|
|
'user.get',
|
|
'db.query',
|
|
() => this.userService.findById(req.params.id)
|
|
);
|
|
|
|
if (!user) {
|
|
return this.handleError(
|
|
new Error('User not found'),
|
|
res,
|
|
'getUser',
|
|
404
|
|
);
|
|
}
|
|
|
|
this.handleSuccess(res, user);
|
|
} catch (error) {
|
|
this.handleError(error, res, 'getUser');
|
|
}
|
|
}
|
|
|
|
async listUsers(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const users = await this.userService.getAll();
|
|
this.handleSuccess(res, users);
|
|
} catch (error) {
|
|
this.handleError(error, res, 'listUsers');
|
|
}
|
|
}
|
|
|
|
async createUser(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
// Validate input with Zod
|
|
const validated = createUserSchema.parse(req.body);
|
|
|
|
// Track performance
|
|
const user = await this.withTransaction(
|
|
'user.create',
|
|
'db.mutation',
|
|
() => this.userService.create(validated)
|
|
);
|
|
|
|
this.handleSuccess(res, user, 'User created successfully', 201);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return this.handleError(error, res, 'createUser', 400);
|
|
}
|
|
this.handleError(error, res, 'createUser');
|
|
}
|
|
}
|
|
|
|
async updateUser(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const validated = updateUserSchema.parse(req.body);
|
|
|
|
const user = await this.userService.update(
|
|
req.params.id,
|
|
validated
|
|
);
|
|
|
|
this.handleSuccess(res, user, 'User updated');
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return this.handleError(error, res, 'updateUser', 400);
|
|
}
|
|
this.handleError(error, res, 'updateUser');
|
|
}
|
|
}
|
|
|
|
async deleteUser(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
await this.userService.delete(req.params.id);
|
|
this.handleSuccess(res, null, 'User deleted', 204);
|
|
} catch (error) {
|
|
this.handleError(error, res, 'deleteUser');
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Complete Service with DI
|
|
|
|
### UserService
|
|
|
|
```typescript
|
|
// services/userService.ts
|
|
import { UserRepository } from '../repositories/UserRepository';
|
|
import { ConflictError, NotFoundError, ValidationError } from '../types/errors';
|
|
import type { CreateUserDTO, UpdateUserDTO, User } from '../types/user.types';
|
|
|
|
export class UserService {
|
|
private userRepository: UserRepository;
|
|
|
|
constructor(userRepository?: UserRepository) {
|
|
this.userRepository = userRepository || new UserRepository();
|
|
}
|
|
|
|
async findById(id: string): Promise<User | null> {
|
|
return await this.userRepository.findById(id);
|
|
}
|
|
|
|
async getAll(): Promise<User[]> {
|
|
return await this.userRepository.findActive();
|
|
}
|
|
|
|
async create(data: CreateUserDTO): Promise<User> {
|
|
// Business rule: validate age
|
|
if (data.age < 18) {
|
|
throw new ValidationError('User must be 18 or older');
|
|
}
|
|
|
|
// Business rule: check email uniqueness
|
|
const existing = await this.userRepository.findByEmail(data.email);
|
|
if (existing) {
|
|
throw new ConflictError('Email already in use');
|
|
}
|
|
|
|
// Create user with profile
|
|
return await this.userRepository.create({
|
|
email: data.email,
|
|
profile: {
|
|
create: {
|
|
firstName: data.firstName,
|
|
lastName: data.lastName,
|
|
age: data.age,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
async update(id: string, data: UpdateUserDTO): Promise<User> {
|
|
// Check exists
|
|
const existing = await this.userRepository.findById(id);
|
|
if (!existing) {
|
|
throw new NotFoundError('User not found');
|
|
}
|
|
|
|
// Business rule: email uniqueness if changing
|
|
if (data.email && data.email !== existing.email) {
|
|
const emailTaken = await this.userRepository.findByEmail(data.email);
|
|
if (emailTaken) {
|
|
throw new ConflictError('Email already in use');
|
|
}
|
|
}
|
|
|
|
return await this.userRepository.update(id, data);
|
|
}
|
|
|
|
async delete(id: string): Promise<void> {
|
|
const existing = await this.userRepository.findById(id);
|
|
if (!existing) {
|
|
throw new NotFoundError('User not found');
|
|
}
|
|
|
|
await this.userRepository.delete(id);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Complete Route File
|
|
|
|
### userRoutes.ts
|
|
|
|
```typescript
|
|
// routes/userRoutes.ts
|
|
import { Router } from 'express';
|
|
import { UserController } from '../controllers/UserController';
|
|
import { SSOMiddlewareClient } from '../middleware/SSOMiddleware';
|
|
import { auditMiddleware } from '../middleware/auditMiddleware';
|
|
|
|
const router = Router();
|
|
const controller = new UserController();
|
|
|
|
// GET /users - List all users
|
|
router.get('/',
|
|
SSOMiddlewareClient.verifyLoginStatus,
|
|
auditMiddleware,
|
|
async (req, res) => controller.listUsers(req, res)
|
|
);
|
|
|
|
// GET /users/:id - Get single user
|
|
router.get('/:id',
|
|
SSOMiddlewareClient.verifyLoginStatus,
|
|
auditMiddleware,
|
|
async (req, res) => controller.getUser(req, res)
|
|
);
|
|
|
|
// POST /users - Create user
|
|
router.post('/',
|
|
SSOMiddlewareClient.verifyLoginStatus,
|
|
auditMiddleware,
|
|
async (req, res) => controller.createUser(req, res)
|
|
);
|
|
|
|
// PUT /users/:id - Update user
|
|
router.put('/:id',
|
|
SSOMiddlewareClient.verifyLoginStatus,
|
|
auditMiddleware,
|
|
async (req, res) => controller.updateUser(req, res)
|
|
);
|
|
|
|
// DELETE /users/:id - Delete user
|
|
router.delete('/:id',
|
|
SSOMiddlewareClient.verifyLoginStatus,
|
|
auditMiddleware,
|
|
async (req, res) => controller.deleteUser(req, res)
|
|
);
|
|
|
|
export default router;
|
|
```
|
|
|
|
---
|
|
|
|
## Complete Repository
|
|
|
|
### UserRepository
|
|
|
|
```typescript
|
|
// repositories/UserRepository.ts
|
|
import { PrismaService } from '@project-lifecycle-portal/database';
|
|
import type { User, Prisma } from '@prisma/client';
|
|
|
|
export class UserRepository {
|
|
async findById(id: string): Promise<User | null> {
|
|
return PrismaService.main.user.findUnique({
|
|
where: { id },
|
|
include: { profile: true },
|
|
});
|
|
}
|
|
|
|
async findByEmail(email: string): Promise<User | null> {
|
|
return PrismaService.main.user.findUnique({
|
|
where: { email },
|
|
include: { profile: true },
|
|
});
|
|
}
|
|
|
|
async findActive(): Promise<User[]> {
|
|
return PrismaService.main.user.findMany({
|
|
where: { isActive: true },
|
|
include: { profile: true },
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
}
|
|
|
|
async create(data: Prisma.UserCreateInput): Promise<User> {
|
|
return PrismaService.main.user.create({
|
|
data,
|
|
include: { profile: true },
|
|
});
|
|
}
|
|
|
|
async update(id: string, data: Prisma.UserUpdateInput): Promise<User> {
|
|
return PrismaService.main.user.update({
|
|
where: { id },
|
|
data,
|
|
include: { profile: true },
|
|
});
|
|
}
|
|
|
|
async delete(id: string): Promise<User> {
|
|
// Soft delete
|
|
return PrismaService.main.user.update({
|
|
where: { id },
|
|
data: {
|
|
isActive: false,
|
|
deletedAt: new Date(),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Refactoring Example: Bad to Good
|
|
|
|
### BEFORE: Business Logic in Routes ❌
|
|
|
|
```typescript
|
|
// routes/postRoutes.ts (BAD - 200+ lines)
|
|
router.post('/posts', async (req, res) => {
|
|
try {
|
|
const username = res.locals.claims.preferred_username;
|
|
const responses = req.body.responses;
|
|
const stepInstanceId = req.body.stepInstanceId;
|
|
|
|
// ❌ Permission check in route
|
|
const userId = await userProfileService.getProfileByEmail(username).then(p => p.id);
|
|
const canComplete = await permissionService.canCompleteStep(userId, stepInstanceId);
|
|
if (!canComplete) {
|
|
return res.status(403).json({ error: 'No permission' });
|
|
}
|
|
|
|
// ❌ Business logic in route
|
|
const post = await postRepository.create({
|
|
title: req.body.title,
|
|
content: req.body.content,
|
|
authorId: userId
|
|
});
|
|
|
|
// ❌ More business logic...
|
|
if (res.locals.isImpersonating) {
|
|
impersonationContextStore.storeContext(...);
|
|
}
|
|
|
|
// ... 100+ more lines
|
|
|
|
res.json({ success: true, data: result });
|
|
} catch (e) {
|
|
handler.handleException(res, e);
|
|
}
|
|
});
|
|
```
|
|
|
|
### AFTER: Clean Separation ✅
|
|
|
|
**1. Clean Route:**
|
|
```typescript
|
|
// routes/postRoutes.ts
|
|
import { PostController } from '../controllers/PostController';
|
|
|
|
const router = Router();
|
|
const controller = new PostController();
|
|
|
|
// ✅ CLEAN: 8 lines total!
|
|
router.post('/',
|
|
SSOMiddlewareClient.verifyLoginStatus,
|
|
auditMiddleware,
|
|
async (req, res) => controller.createPost(req, res)
|
|
);
|
|
|
|
export default router;
|
|
```
|
|
|
|
**2. Controller:**
|
|
```typescript
|
|
// controllers/PostController.ts
|
|
export class PostController extends BaseController {
|
|
private postService: PostService;
|
|
|
|
constructor() {
|
|
super();
|
|
this.postService = new PostService();
|
|
}
|
|
|
|
async createPost(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const validated = createPostSchema.parse({
|
|
...req.body,
|
|
});
|
|
|
|
const result = await this.postService.createPost(
|
|
validated,
|
|
res.locals.userId
|
|
);
|
|
|
|
this.handleSuccess(res, result, 'Post created successfully');
|
|
} catch (error) {
|
|
this.handleError(error, res, 'createPost');
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**3. Service:**
|
|
```typescript
|
|
// services/postService.ts
|
|
export class PostService {
|
|
async createPost(
|
|
data: CreatePostDTO,
|
|
userId: string
|
|
): Promise<SubmissionResult> {
|
|
// Permission check
|
|
const canComplete = await permissionService.canCompleteStep(
|
|
userId,
|
|
data.stepInstanceId
|
|
);
|
|
|
|
if (!canComplete) {
|
|
throw new ForbiddenError('No permission to complete step');
|
|
}
|
|
|
|
// Execute workflow
|
|
const engine = await createWorkflowEngine();
|
|
const command = new CompleteStepCommand(
|
|
data.stepInstanceId,
|
|
userId,
|
|
data.responses
|
|
);
|
|
const events = await engine.executeCommand(command);
|
|
|
|
// Handle impersonation
|
|
if (context.isImpersonating) {
|
|
await this.handleImpersonation(data.stepInstanceId, context);
|
|
}
|
|
|
|
return { events, success: true };
|
|
}
|
|
|
|
private async handleImpersonation(stepInstanceId: number, context: any) {
|
|
impersonationContextStore.storeContext(stepInstanceId, {
|
|
originalUserId: context.originalUserId,
|
|
effectiveUserId: context.effectiveUserId,
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
**Result:**
|
|
- Route: 8 lines (was 200+)
|
|
- Controller: 25 lines
|
|
- Service: 40 lines
|
|
- **Testable, maintainable, reusable!**
|
|
|
|
---
|
|
|
|
## End-to-End Feature Example
|
|
|
|
### Complete User Management Feature
|
|
|
|
**1. Types:**
|
|
```typescript
|
|
// types/user.types.ts
|
|
export interface User {
|
|
id: string;
|
|
email: string;
|
|
isActive: boolean;
|
|
profile?: UserProfile;
|
|
}
|
|
|
|
export interface CreateUserDTO {
|
|
email: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
age: number;
|
|
}
|
|
|
|
export interface UpdateUserDTO {
|
|
email?: string;
|
|
firstName?: string;
|
|
lastName?: string;
|
|
}
|
|
```
|
|
|
|
**2. Validators:**
|
|
```typescript
|
|
// validators/userSchemas.ts
|
|
import { z } from 'zod';
|
|
|
|
export const createUserSchema = z.object({
|
|
email: z.string().email(),
|
|
firstName: z.string().min(1).max(100),
|
|
lastName: z.string().min(1).max(100),
|
|
age: z.number().int().min(18).max(120),
|
|
});
|
|
|
|
export const updateUserSchema = z.object({
|
|
email: z.string().email().optional(),
|
|
firstName: z.string().min(1).max(100).optional(),
|
|
lastName: z.string().min(1).max(100).optional(),
|
|
});
|
|
```
|
|
|
|
**3. Repository:**
|
|
```typescript
|
|
// repositories/UserRepository.ts
|
|
export class UserRepository {
|
|
async findById(id: string): Promise<User | null> {
|
|
return PrismaService.main.user.findUnique({
|
|
where: { id },
|
|
include: { profile: true },
|
|
});
|
|
}
|
|
|
|
async create(data: Prisma.UserCreateInput): Promise<User> {
|
|
return PrismaService.main.user.create({
|
|
data,
|
|
include: { profile: true },
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
**4. Service:**
|
|
```typescript
|
|
// services/userService.ts
|
|
export class UserService {
|
|
private userRepository: UserRepository;
|
|
|
|
constructor() {
|
|
this.userRepository = new UserRepository();
|
|
}
|
|
|
|
async create(data: CreateUserDTO): Promise<User> {
|
|
const existing = await this.userRepository.findByEmail(data.email);
|
|
if (existing) {
|
|
throw new ConflictError('Email already exists');
|
|
}
|
|
|
|
return await this.userRepository.create({
|
|
email: data.email,
|
|
profile: {
|
|
create: {
|
|
firstName: data.firstName,
|
|
lastName: data.lastName,
|
|
age: data.age,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
**5. Controller:**
|
|
```typescript
|
|
// controllers/UserController.ts
|
|
export class UserController extends BaseController {
|
|
private userService: UserService;
|
|
|
|
constructor() {
|
|
super();
|
|
this.userService = new UserService();
|
|
}
|
|
|
|
async createUser(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const validated = createUserSchema.parse(req.body);
|
|
const user = await this.userService.create(validated);
|
|
this.handleSuccess(res, user, 'User created', 201);
|
|
} catch (error) {
|
|
this.handleError(error, res, 'createUser');
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**6. Routes:**
|
|
```typescript
|
|
// routes/userRoutes.ts
|
|
const router = Router();
|
|
const controller = new UserController();
|
|
|
|
router.post('/',
|
|
SSOMiddlewareClient.verifyLoginStatus,
|
|
async (req, res) => controller.createUser(req, res)
|
|
);
|
|
|
|
export default router;
|
|
```
|
|
|
|
**7. Register in app.ts:**
|
|
```typescript
|
|
// app.ts
|
|
import userRoutes from './routes/userRoutes';
|
|
|
|
app.use('/api/users', userRoutes);
|
|
```
|
|
|
|
**Complete Request Flow:**
|
|
```
|
|
POST /api/users
|
|
↓
|
|
userRoutes matches /
|
|
↓
|
|
SSOMiddleware authenticates
|
|
↓
|
|
controller.createUser called
|
|
↓
|
|
Validates with Zod
|
|
↓
|
|
userService.create called
|
|
↓
|
|
Checks business rules
|
|
↓
|
|
userRepository.create called
|
|
↓
|
|
Prisma creates user
|
|
↓
|
|
Returns up the chain
|
|
↓
|
|
Controller formats response
|
|
↓
|
|
200/201 sent to client
|
|
```
|
|
|
|
---
|
|
|
|
**Related Files:**
|
|
- SKILL.md
|
|
- [routing-and-controllers.md](routing-and-controllers.md)
|
|
- [services-and-repositories.md](services-and-repositories.md)
|
|
- [validation-patterns.md](validation-patterns.md)
|