C
Controllers & Services
Clean architecture with typed controllers, services, and repository patterns in Express.
Code
typescript
1// types/user.ts2export interface User {3 id: string;4 name: string;5 email: string;6 passwordHash: string;7 role: "admin" | "user";8 createdAt: Date;9 updatedAt: Date;10}11 12export interface CreateUserInput {13 name: string;14 email: string;15 password: string;16 role?: "admin" | "user";17}18 19export interface UpdateUserInput {20 name?: string;21 email?: string;22 role?: "admin" | "user";23}24 25// repositories/user.repository.ts26export interface IUserRepository {27 findAll(): Promise<User[]>;28 findById(id: string): Promise<User | null>;29 findByEmail(email: string): Promise<User | null>;30 create(data: Omit<User, "id" | "createdAt" | "updatedAt">): Promise<User>;31 update(id: string, data: Partial<User>): Promise<User | null>;32 delete(id: string): Promise<boolean>;33}34 35class UserRepository implements IUserRepository {36 private users: User[] = [];37 38 async findAll(): Promise<User[]> {39 return this.users;40 }41 42 async findById(id: string): Promise<User | null> {43 return this.users.find((u) => u.id === id) || null;44 }45 46 async findByEmail(email: string): Promise<User | null> {47 return this.users.find((u) => u.email === email) || null;48 }49 50 async create(data: Omit<User, "id" | "createdAt" | "updatedAt">): Promise<User> {51 const user: User = {52 ...data,53 id: `user_${Date.now()}`,54 createdAt: new Date(),55 updatedAt: new Date(),56 };57 this.users.push(user);58 return user;59 }60 61 async update(id: string, data: Partial<User>): Promise<User | null> {62 const index = this.users.findIndex((u) => u.id === id);63 if (index === -1) return null;64 65 this.users[index] = {66 ...this.users[index],67 ...data,68 updatedAt: new Date(),69 };70 return this.users[index];71 }72 73 async delete(id: string): Promise<boolean> {74 const index = this.users.findIndex((u) => u.id === id);75 if (index === -1) return false;76 77 this.users.splice(index, 1);78 return true;79 }80}81 82// services/user.service.ts83import bcrypt from "bcrypt";84 85class UserService {86 constructor(private userRepository: IUserRepository) {}87 88 async getAllUsers(): Promise<Omit<User, "passwordHash">[]> {89 const users = await this.userRepository.findAll();90 return users.map(({ passwordHash, ...user }) => user);91 }92 93 async getUserById(id: string): Promise<Omit<User, "passwordHash"> | null> {94 const user = await this.userRepository.findById(id);95 if (!user) return null;96 97 const { passwordHash, ...userData } = user;98 return userData;99 }100 101 async createUser(input: CreateUserInput): Promise<Omit<User, "passwordHash">> {102 // Check if email already exists103 const existingUser = await this.userRepository.findByEmail(input.email);104 if (existingUser) {105 throw new Error("Email already in use");106 }107 108 // Hash password109 const passwordHash = await bcrypt.hash(input.password, 10);110 111 const user = await this.userRepository.create({112 name: input.name,113 email: input.email,114 passwordHash,115 role: input.role || "user",116 });117 118 const { passwordHash: _, ...userData } = user;119 return userData;120 }121 122 async updateUser(123 id: string,124 input: UpdateUserInput125 ): Promise<Omit<User, "passwordHash"> | null> {126 const user = await this.userRepository.update(id, input);127 if (!user) return null;128 129 const { passwordHash, ...userData } = user;130 return userData;131 }132 133 async deleteUser(id: string): Promise<boolean> {134 return this.userRepository.delete(id);135 }136}137 138// controllers/user.controller.ts139import { Request, Response, NextFunction } from "express";140 141class UserController {142 constructor(private userService: UserService) {}143 144 getAll = async (req: Request, res: Response, next: NextFunction) => {145 try {146 const users = await this.userService.getAllUsers();147 res.json(users);148 } catch (error) {149 next(error);150 }151 };152 153 getById = async (req: Request, res: Response, next: NextFunction) => {154 try {155 const user = await this.userService.getUserById(req.params.id);156 157 if (!user) {158 res.status(404).json({ error: "User not found" });159 return;160 }161 162 res.json(user);163 } catch (error) {164 next(error);165 }166 };167 168 create = async (req: Request, res: Response, next: NextFunction) => {169 try {170 const user = await this.userService.createUser(req.body);171 res.status(201).json(user);172 } catch (error) {173 if (error instanceof Error && error.message === "Email already in use") {174 res.status(409).json({ error: error.message });175 return;176 }177 next(error);178 }179 };180 181 update = async (req: Request, res: Response, next: NextFunction) => {182 try {183 const user = await this.userService.updateUser(req.params.id, req.body);184 185 if (!user) {186 res.status(404).json({ error: "User not found" });187 return;188 }189 190 res.json(user);191 } catch (error) {192 next(error);193 }194 };195 196 delete = async (req: Request, res: Response, next: NextFunction) => {197 try {198 const deleted = await this.userService.deleteUser(req.params.id);199 200 if (!deleted) {201 res.status(404).json({ error: "User not found" });202 return;203 }204 205 res.status(204).send();206 } catch (error) {207 next(error);208 }209 };210}211 212// Dependency injection setup213const userRepository = new UserRepository();214const userService = new UserService(userRepository);215const userController = new UserController(userService);216 217export { userController, userService, userRepository };Run this example locally
$ npm install express bcrypt @types/express @types/bcrypt