← Назад к вопросам

Можно ли унифицировать 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/55/51/5❌ Нет
Helper (asyncHandler)4/54/52/5⚠️ Для малых
Repository Pattern3/54/54/5✅ Рекомендуется
Controller Class3/54/54/5✅ Рекомендуется
Router Builder4/53/53/5✅ Для простых
GraphQL2/55/55/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 и читаемостью.

Можно ли унифицировать CRUD операции? | PrepBro