Биз жөнүндөБлогБайланыш
Иштеп чыгуу2026-ж., 18-апрель 5 мин 8

Node.js'теги таза архитектура: бизнес логиканы инфраструктурадан бөлүп алуу

AunimedaAunimeda
📋 Мазмуну

Node.js'теги таза архитектура: бизнес логиканы инфраструктурадан бөлүп алуу

Node.js долбоорунун типтүү жолу: баары сонун башталат, алты айдан кийин бизнес логика контроллерлерге чачырап кетет, сервистер Prisma'ны түздөн-түз импорттойт, тесттер реалдуу маалымат базасын иштетүүнү талап кылат. Бул чечилет - бир нече принциптерди башынан колдонуу менен.


Негизги принцип

Бизнес логика маалымат базасына, фреймворкка же тышкы кызматка көз каранды болбошу керек.

Заказды иштетүү кодуңуз Prisma же Express'тин бар экенин билбеши керек. Ал бизнес эрежелерди гана билиши керек: наркты кантип эсептөө, заказды жокко чыгарса болобу, төлөм болгондо эмне болот.

Эмне үчүн маанилүү:

  • Маалымат базасыз бизнес логиканы тестирлесе болот
  • Prisma'дан Drizzle'га өтүү бизнес логиканы өзгөртпөйт
  • Логика бир жерде, хендлерлерге чачырабаган

Иштеген папка структурасы

src/
├── domain/              # Таза бизнес логика
│   ├── entities/        # Методдор менен сущностор
│   │   └── Order.ts
│   └── repositories/    # Интерфейстер
│       └── IOrderRepository.ts
│
├── application/         # Use cases - тиркеме эмне кылат
│   └── orders/
│       ├── CreateOrder.ts
│       └── CancelOrder.ts
│
├── infrastructure/      # Конкреттүү реализациялар
│   └── database/
│       └── PrismaOrderRepository.ts
│
└── presentation/        # Express routes жана controllers
    └── http/
        └── OrderController.ts

Кирешелүүлүк эрежеси: domainapplicationinfrastructure/presentation


Домен сущностасы

// src/domain/entities/Order.ts
export class Order {
  constructor(
    readonly id: string,
    readonly userId: string,
    private _items: OrderItem[],
    private _status: 'pending' | 'paid' | 'shipped' | 'cancelled' = 'pending',
  ) {}

  get status() { return this._status; }
  get items() { return [...this._items]; }
  get total() {
    return this._items.reduce((sum, i) => sum + i.price * i.quantity, 0);
  }

  // Бизнес эреже: күтүп жаткан заказдарды гана жокко чыгарса болот
  cancel() {
    if (this._status !== 'pending') {
      throw new Error(`"${this._status}" статусундагы заказды жокко чыгаруу мүмкүн эмес`);
    }
    this._status = 'cancelled';
  }

  // Бизнес эреже: төлөмдөн кийин жеткирилди деп белгилөө
  markShipped() {
    if (this._status !== 'paid') {
      throw new Error('Жеткирүүдөн мурун заказ төлөнүшү керек');
    }
    this._status = 'shipped';
  }
}

export interface OrderItem {
  productId: string;
  name: string;
  price: number;
  quantity: number;
}

Эч кандай импорт жок. Таза TypeScript. Маалымат базасыз тестирленет.


Репозиторий интерфейси

// src/domain/repositories/IOrderRepository.ts
import { Order } from '../entities/Order';

export interface IOrderRepository {
  findById(id: string): Promise<Order | null>;
  findByUserId(userId: string): Promise<Order[]>;
  save(order: Order): Promise<void>;
  update(order: Order): Promise<void>;
}

Бул контракт. Application катмары Prisma'га эмес, ушул интерфейске көз каранды.


Use Case

// src/application/orders/CreateOrder.ts
import { Order } from '../../domain/entities/Order';
import { IOrderRepository } from '../../domain/repositories/IOrderRepository';
import { IProductRepository } from '../../domain/repositories/IProductRepository';
import { randomUUID } from 'crypto';

export class CreateOrderUseCase {
  constructor(
    private orders: IOrderRepository,
    private products: IProductRepository,
  ) {}

  async execute(input: {
    userId: string;
    items: Array<{ productId: string; quantity: number }>;
  }) {
    // Продуктылардын болушун жана запасын текшерүү
    const orderItems = await Promise.all(
      input.items.map(async (item) => {
        const product = await this.products.findById(item.productId);
        if (!product) throw new Error(`Продукт табылган жок: ${item.productId}`);
        if (product.stock < item.quantity) {
          throw new Error(`Товар жетишсиз: ${product.name}`);
        }
        return {
          productId: product.id,
          name: product.name,
          price: product.price,
          quantity: item.quantity,
        };
      })
    );

    const order = new Order(randomUUID(), input.userId, orderItems);
    await this.orders.save(order);

    return { orderId: order.id, total: order.total };
  }
}

Маалымат базасыз тест:

describe('CreateOrderUseCase', () => {
  it('туура суммасы менен заказ түзөт', async () => {
    const orders = {
      save: jest.fn(),
      findById: jest.fn(),
      findByUserId: jest.fn(),
      update: jest.fn(),
    };
    const products = {
      findById: jest.fn().mockResolvedValue({
        id: 'p1', name: 'Товар', price: 1000, stock: 5
      }),
    };

    const useCase = new CreateOrderUseCase(orders, products);
    const result = await useCase.execute({
      userId: 'u1',
      items: [{ productId: 'p1', quantity: 3 }],
    });

    expect(result.total).toBe(3000);
    expect(orders.save).toHaveBeenCalledTimes(1);
  });
});

Prisma репозиторийинин реализациясы

// src/infrastructure/database/PrismaOrderRepository.ts
import { PrismaClient } from '@prisma/client';
import { Order } from '../../domain/entities/Order';
import { IOrderRepository } from '../../domain/repositories/IOrderRepository';

export class PrismaOrderRepository implements IOrderRepository {
  constructor(private prisma: PrismaClient) {}

  async save(order: Order): Promise<void> {
    await this.prisma.order.create({
      data: {
        id: order.id,
        userId: order.userId,
        status: order.status,
        total: order.total,
        items: {
          createMany: {
            data: order.items.map(i => ({
              productId: i.productId,
              name: i.name,
              price: i.price,
              quantity: i.quantity,
            })),
          },
        },
      },
    });
  }

  async findById(id: string): Promise<Order | null> {
    const r = await this.prisma.order.findUnique({ where: { id }, include: { items: true } });
    return r ? new Order(r.id, r.userId, r.items, r.status as any) : null;
  }

  async findByUserId(userId: string): Promise<Order[]> {
    const records = await this.prisma.order.findMany({
      where: { userId }, include: { items: true },
    });
    return records.map(r => new Order(r.id, r.userId, r.items, r.status as any));
  }

  async update(order: Order): Promise<void> {
    await this.prisma.order.update({ where: { id: order.id }, data: { status: order.status } });
  }
}

Dependency Injection жана контроллер

// src/container.ts
import { PrismaClient } from '@prisma/client';
import { PrismaOrderRepository } from './infrastructure/database/PrismaOrderRepository';
import { PrismaProductRepository } from './infrastructure/database/PrismaProductRepository';
import { CreateOrderUseCase } from './application/orders/CreateOrderUseCase';

const prisma = new PrismaClient();

export const useCases = {
  createOrder: new CreateOrderUseCase(
    new PrismaOrderRepository(prisma),
    new PrismaProductRepository(prisma),
  ),
};
// HTTP контроллер - HTTP жөнүндө гана билет
export async function createOrder(req: Request, res: Response) {
  const schema = z.object({
    items: z.array(z.object({ productId: z.string().uuid(), quantity: z.number().int().min(1) })),
  });
  
  const parsed = schema.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });

  try {
    const result = await useCases.createOrder.execute({
      userId: req.user.id,
      items: parsed.data.items,
    });
    res.status(201).json(result);
  } catch (err: any) {
    res.status(422).json({ error: err.message });
  }
}

Качан колдонуу керек

Бул ыкма кошумча катмарлар кошот. Мындай болгондо колдонуңуз:

  • Бизнес логика татаалдашып жатат
  • Маалымат базасыз логиканы тестирлөө керек
  • Бир нече чакыруу булактары (HTTP + кезек)
  • Командада бирден ашык разработчик

Жөнөкөй CRUD үчүн - жакшы уюштурулган Express жетиштүү.


Aunimeda продакшн Node.js архитектурасын долбоорлойт жана курат.

Ошондой эле: Telegram Bot FSM, Git продвинутый workflow

Ошондой эле окуңуз

WebSockets vs SSE vs Long Polling: realtime технологиясын кантип тандоо керекaunimeda
Иштеп чыгуу

WebSockets vs SSE vs Long Polling: realtime технологиясын кантип тандоо керек

Чат, кабарлар, заказ статусу — булардын баары реалдуу убакытта жаңыртууну талап кылат. WebSocket, Server-Sent Events жана Long Polling ар башка иштейт. Кайсын качан колдонуу керегин Node.js код мисалдары менен карайбыз.

Node.js vs Bun vs Deno 2026: кайсы JavaScript runtime тандоо керекaunimeda
Иштеп чыгуу

Node.js vs Bun vs Deno 2026: кайсы JavaScript runtime тандоо керек

Bun 1.x продакшн стабилдүү. Deno 2.0 npm колдойт. Node.js 22 TypeScript'ти нативдүү иштетет. Реалдуу benchmark'тар, экосистема салыштыруусу жана Кыргызстандагы долбоорлор үчүн конкреттүү сунуштамалар.

Supabase vs Firebase: Кыргызстан стартабы үчүн кайсын тандоо керекaunimeda
Иштеп чыгуу

Supabase vs Firebase: Кыргызстан стартабы үчүн кайсын тандоо керек

Supabase — PostgreSQL менен open-source BaaS. Firebase — Google'дун зрелый платформасы. PocketBase — бир бинардык файл, MVP үчүн идеал. Маалымат моделдери, баалар, realtime жана Кыргызстан долбоорлору үчүн кайсы тандоо туура экенин карайбыз.

Бизнесиңизге IT иштеп чыгуу керекпи?

Веб-сайттарды, мобилдик тиркемелерди жана AI чечимдерин иштеп чыгабыз. Акысыз консультация.

Консультация алуу Бардык макалалар