← Назад к вопросам
Можно ли унифицировать CRUD операции?
1.8 Middle🔥 182 комментариев
#Архитектура и паттерны#ООП
Комментарии (2)
🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Можно ли унифицировать CRUD операции?
Ответ: Да, абсолютно можно и нужно. Это один из ключей к масштабируемому и maintainable коду. Расскажу несколько подходов от простого к сложному.
Основная проблема дублирования
❌ Без унификации (код повторяется)
// routes/users.js
app.post('/api/users', async (req, res) => {
try {
const user = await db.users.create(req.body);
res.status(201).json(user);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
app.get('/api/users/:id', async (req, res) => {
try {
const user = await db.users.findById(req.params.id);
res.json(user);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
app.put('/api/users/:id', async (req, res) => {
try {
const user = await db.users.update(req.params.id, req.body);
res.json(user);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
app.delete('/api/users/:id', async (req, res) => {
try {
await db.users.delete(req.params.id);
res.status(204).send();
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// routes/products.js
// ВСЕ ТО ЖЕ САМОЕ ПОВТОРЯЕТСЯ!
app.post('/api/products', async (req, res) => {
try {
const product = await db.products.create(req.body);
res.status(201).json(product);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// ... и так дальше для каждой сущности
Подход 1: Базовый Helper (простейший)
Решение: Factory функция
// middleware/asyncHandler.js
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
module.exports = asyncHandler;
// routes/users.js
const asyncHandler = require('../middleware/asyncHandler');
app.post('/api/users', asyncHandler(async (req, res) => {
const user = await db.users.create(req.body);
res.status(201).json(user);
}));
app.get('/api/users/:id', asyncHandler(async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json(user);
}));
// Меньше дублирования try-catch!
Подход 2: Repository Pattern (рекомендуемый)
Базовый Repository
// repositories/BaseRepository.js
class BaseRepository {
constructor(model) {
this.model = model;
}
async create(data) {
return await this.model.create(data);
}
async findById(id) {
return await this.model.findById(id);
}
async findAll(filters = {}) {
return await this.model.find(filters);
}
async update(id, data) {
return await this.model.findByIdAndUpdate(
id,
{ $set: data },
{ new: true }
);
}
async delete(id) {
return await this.model.findByIdAndDelete(id);
}
}
module.exports = BaseRepository;
// repositories/UserRepository.js
const BaseRepository = require('./BaseRepository');
const User = require('../models/User');
class UserRepository extends BaseRepository {
constructor() {
super(User);
}
// Специфичные для User методы
async findByEmail(email) {
return await this.model.findOne({ email });
}
async findAdults() {
return await this.model.find({ age: { $gte: 18 } });
}
}
module.exports = new UserRepository();
Unified Routes (через Repository)
// routes/crud.js
const express = require('express');
const asyncHandler = require('../middleware/asyncHandler');
const createCrudRoutes = (router, resource, repository) => {
// Create
router.post(`/${resource}`, asyncHandler(async (req, res) => {
const item = await repository.create(req.body);
res.status(201).json(item);
}));
// Read one
router.get(`/${resource}/:id`, asyncHandler(async (req, res) => {
const item = await repository.findById(req.params.id);
if (!item) {
return res.status(404).json({ error: 'Not found' });
}
res.json(item);
}));
// Read all
router.get(`/${resource}`, asyncHandler(async (req, res) => {
const items = await repository.findAll(req.query);
res.json(items);
}));
// Update
router.put(`/${resource}/:id`, asyncHandler(async (req, res) => {
const item = await repository.update(req.params.id, req.body);
if (!item) {
return res.status(404).json({ error: 'Not found' });
}
res.json(item);
}));
// Delete
router.delete(`/${resource}/:id`, asyncHandler(async (req, res) => {
const item = await repository.delete(req.params.id);
if (!item) {
return res.status(404).json({ error: 'Not found' });
}
res.status(204).send();
}));
return router;
};
module.exports = createCrudRoutes;
// app.js
const express = require('express');
const createCrudRoutes = require('./routes/crud');
const userRepository = require('./repositories/UserRepository');
const productRepository = require('./repositories/ProductRepository');
const app = express();
// Создаем CRUD routes для каждого ресурса
const userRouter = express.Router();
const productRouter = express.Router();
create CrudRoutes(userRouter, 'users', userRepository);
createCrudRoutes(productRouter, 'products', productRepository);
app.use('/api', userRouter);
app.use('/api', productRouter);
Подход 3: Controller Class (ООП)
// controllers/BaseController.js
class BaseController {
constructor(repository) {
this.repository = repository;
}
async create(req, res) {
const item = await this.repository.create(req.body);
res.status(201).json(item);
}
async findById(req, res) {
const item = await this.repository.findById(req.params.id);
if (!item) {
return res.status(404).json({ error: 'Not found' });
}
res.json(item);
}
async findAll(req, res) {
const items = await this.repository.findAll(req.query);
res.json(items);
}
async update(req, res) {
const item = await this.repository.update(req.params.id, req.body);
if (!item) {
return res.status(404).json({ error: 'Not found' });
}
res.json(item);
}
async delete(req, res) {
await this.repository.delete(req.params.id);
res.status(204).send();
}
}
module.exports = BaseController;
// controllers/UserController.js
const BaseController = require('./BaseController');
const userRepository = require('../repositories/UserRepository');
class UserController extends BaseController {
constructor() {
super(userRepository);
}
// Специфичные для User методы
async findAdults(req, res) {
const adults = await this.repository.findAdults();
res.json(adults);
}
async findByEmail(req, res) {
const user = await this.repository.findByEmail(req.query.email);
res.json(user);
}
}
module.exports = new UserController();
// routes/users.js
const express = require('express');
const userController = require('../controllers/UserController');
const router = express.Router();
router.post('/', (req, res) => userController.create(req, res));
router.get('/', (req, res) => userController.findAll(req, res));
router.get('/adults', (req, res) => userController.findAdults(req, res));
router.get('/:id', (req, res) => userController.findById(req, res));
router.put('/:id', (req, res) => userController.update(req, res));
router.delete('/:id', (req, res) => userController.delete(req, res));
module.exports = router;
Подход 4: Express Router Builder (функциональный)
// utils/createResourceRouter.js
const express = require('express');
const asyncHandler = require('../middleware/asyncHandler');
const createResourceRouter = (resource, repository) => {
const router = express.Router();
router
.post(
'/',
asyncHandler(async (req, res) => {
const item = await repository.create(req.body);
res.status(201).json(item);
})
)
.get(
'/',
asyncHandler(async (req, res) => {
const items = await repository.findAll(req.query);
res.json(items);
})
)
.get(
'/:id',
asyncHandler(async (req, res) => {
const item = await repository.findById(req.params.id);
res.json(item || { error: 'Not found' });
})
)
.put(
'/:id',
asyncHandler(async (req, res) => {
const item = await repository.update(req.params.id, req.body);
res.json(item);
})
)
.delete(
'/:id',
asyncHandler(async (req, res) => {
await repository.delete(req.params.id);
res.status(204).send();
})
);
return router;
};
module.exports = createResourceRouter;
// app.js
const express = require('express');
const createResourceRouter = require('./utils/createResourceRouter');
const userRepository = require('./repositories/UserRepository');
const productRepository = require('./repositories/ProductRepository');
const app = express();
app.use('/api/users', createResourceRouter('users', userRepository));
app.use('/api/products', createResourceRouter('products', productRepository));
Подход 5: GraphQL (идеальная унификация)
// schema.graphql
type User {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}
type Mutation {
createUser(input: UserInput!): User!
updateUser(id: ID!, input: UserInput!): User!
deleteUser(id: ID!): Boolean!
}
input UserInput {
name: String!
email: String!
}
// resolvers.js
const resolvers = {
Query: {
user: (_, { id }) => userRepository.findById(id),
users: () => userRepository.findAll()
},
Mutation: {
createUser: (_, { input }) => userRepository.create(input),
updateUser: (_, { id, input }) => userRepository.update(id, input),
deleteUser: (_, { id }) => userRepository.delete(id)
}
};
// Одна реализация для всех ресурсов!
Сравнение подходов
| Подход | Простота | Гибкость | Масштабируемость | Рекомендуется |
|---|---|---|---|---|
| Без унификации | 5/5 | 5/5 | 1/5 | ❌ Нет |
| Helper (asyncHandler) | 4/5 | 4/5 | 2/5 | ⚠️ Для малых |
| Repository Pattern | 3/5 | 4/5 | 4/5 | ✅ Рекомендуется |
| Controller Class | 3/5 | 4/5 | 4/5 | ✅ Рекомендуется |
| Router Builder | 4/5 | 3/5 | 3/5 | ✅ Для простых |
| GraphQL | 2/5 | 5/5 | 5/5 | ✅ Для крупных |
Мой рекомендуемый стек (для большинства проектов)
// 1. Base Repository (для DB операций)
class BaseRepository { /* ... */ }
// 2. Base Controller (для HTTP логики)
class BaseController { /* ... */ }
// 3. Express routes (с использованием controller)
router.post('/', asyncHandler((req, res) => controller.create(req, res)));
// 4. Специфичные методы в подклассах
class UserController extends BaseController {
async findAdults(req, res) { /* ... */ }
}
Практический пример: Полная система
// models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: String,
email: String,
age: Number
});
module.exports = mongoose.model('User', userSchema);
// repositories/UserRepository.js
const BaseRepository = require('./BaseRepository');
const User = require('../models/User');
class UserRepository extends BaseRepository {
constructor() { super(User); }
async findAdults() { return this.model.find({ age: { $gte: 18 } }); }
}
module.exports = new UserRepository();
// controllers/UserController.js
const BaseController = require('./BaseController');
const userRepository = require('../repositories/UserRepository');
class UserController extends BaseController {
constructor() { super(userRepository); }
async findAdults(req, res) {
const adults = await this.repository.findAdults();
res.json(adults);
}
}
module.exports = new UserController();
// routes/users.js
const express = require('express');
const userController = require('../controllers/UserController');
const asyncHandler = require('../middleware/asyncHandler');
const router = express.Router();
router.post('/', asyncHandler((req, res) => userController.create(req, res)));
router.get('/', asyncHandler((req, res) => userController.findAll(req, res)));
router.get('/adults', asyncHandler((req, res) => userController.findAdults(req, res)));
router.get('/:id', asyncHandler((req, res) => userController.findById(req, res)));
router.put('/:id', asyncHandler((req, res) => userController.update(req, res)));
router.delete('/:id', asyncHandler((req, res) => userController.delete(req, res)));
module.exports = router;
// app.js
const express = require('express');
const userRoutes = require('./routes/users');
const productRoutes = require('./routes/products');
const app = express();
app.use(express.json());
app.use('/api/users', userRoutes);
app.use('/api/products', productRoutes);
app.listen(3000);
Итоги
Да, CRUD операции можно и нужно унифицировать:
✅ Преимущества:
- DRY (Don't Repeat Yourself)
- Меньше кода
- Легче обновлять
- Меньше ошибок
- Лучше для новых разработчиков
❌ Недостатки при over-engineering:
- Может быть сложнее понять
- Абстракция может скрыть проблемы
Рекомендация: Используй Repository Pattern + Base Controller. Это баланс между DRY и читаемостью.