C

Controllers & Services

Clean architecture with typed controllers, services, and repository patterns in Express.

Code

typescript
1// types/user.ts
2export 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.ts
26export 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.ts
83import 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 exists
103 const existingUser = await this.userRepository.findByEmail(input.email);
104 if (existingUser) {
105 throw new Error("Email already in use");
106 }
107
108 // Hash password
109 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: UpdateUserInput
125 ): 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.ts
139import { 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 setup
213const 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