Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Service Layer no Node.js

Atualizado em: 2 de setembro de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

O Service Layer no Node.js organiza os casos de uso da aplicação em uma camada explícita entre os controllers e o domínio ou infraestrutura. Em vez de controllers executarem regras, abrirem transações, chamarem vários repositories e decidirem respostas de negócio, eles delegam a operação a um serviço de aplicação.

Essa camada melhora a separação de responsabilidades, facilita testes e evita duplicação entre HTTP, filas, tarefas agendadas e comandos internos. Porém, um Service Layer não deve virar uma coleção de classes gigantes ou um simples espelho de métodos CRUD. Ele precisa representar intenções reais, como aprovar pedido, registrar pagamento ou suspender usuário.

Neste guia, você aprenderá a desenhar serviços de aplicação, diferenciar domínio e infraestrutura, usar repositories, transações, validação, idempotência, autorização, eventos, testes e observabilidade.

O que é Service Layer?

Service Layer define a fronteira das operações disponíveis para consumidores da aplicação. O padrão é descrito no catálogo Patterns of Enterprise Application Architecture. Ele coordena respostas do domínio e controla a unidade de trabalho.

O serviço pode ser chamado por um controller HTTP, um consumidor de fila, uma CLI ou uma rotina interna. A regra central permanece igual.

Para persistência isolada, consulte Repository Pattern no Node.js. Para composição de objetos, veja Injeção de Dependência no Node.js.

Controller sem Service Layer

app.post('/orders/:id/approve', async (req, res) => {
  const order = await prisma.order.findUnique({
    where: { id: req.params.id }
  });

  if (!order) {
    return res.status(404).json({ code: 'NOT_FOUND' });
  }

  if (order.status !== 'pending') {
    return res.status(409).json({ code: 'INVALID_STATUS' });
  }

  await prisma.order.update({
    where: { id: order.id },
    data: { status: 'approved' }
  });

  await queue.publish('order.approved', { id: order.id });
  res.status(204).end();
});

O controller conhece persistência, regra, evento e detalhes de erro. Reutilizar o mesmo fluxo em uma fila exige duplicação.

Controller fino

app.post('/orders/:id/approve', async (req, res) => {
  const command = {
    orderId: req.params.id,
    actorId: req.user.id
  };

  await approveOrder.execute(command);
  res.status(204).end();
});

O controller traduz HTTP para entrada do caso de uso e transforma erros conhecidos em respostas.

Serviço de aplicação

export class ApproveOrder {
  constructor(
    private readonly orders: OrderRepository,
    private readonly authorization: AuthorizationService,
    private readonly unitOfWork: UnitOfWork
  ) {}

  async execute(input: ApproveOrderInput): Promise<void> {
    await this.authorization.assertCanApprove(
      input.actorId,
      input.orderId
    );

    await this.unitOfWork.run(async tx => {
      const order = await tx.orders.findById(input.orderId);

      if (!order) {
        throw new OrderNotFoundError(input.orderId);
      }

      order.approve();
      await tx.orders.save(order);
      await tx.outbox.add(order.pullEvents());
    });
  }
}

O serviço coordena autorização, transação, entidade e outbox.

Serviço de aplicação versus serviço de domínio

Um serviço de aplicação orquestra o caso de uso. Um serviço de domínio contém uma regra que não pertence naturalmente a uma única entidade.

class PricingService {
  calculate(order: Order, customer: Customer): Money {
    // regra de preço entre agregados
  }
}

O serviço de aplicação chama o serviço de domínio e repositories. Não coloque detalhes HTTP no domínio.

Nome por intenção

Prefira:

  • ApproveOrder;
  • CancelSubscription;
  • RegisterPayment;
  • ResetUserPassword;
  • GenerateMonthlyInvoice.

Evite um único OrderService com dezenas de métodos sem coesão.

Entrada explícita

type ApproveOrderInput = {
  orderId: string;
  actorId: string;
  idempotencyKey?: string;
};

Não passe o objeto req para o serviço. Isso acopla a camada ao framework.

Saída explícita

type ApproveOrderOutput = {
  id: string;
  status: 'approved';
  approvedAt: Date;
};

A saída pode ser um DTO, não necessariamente a entidade completa.

Validação de formato

Controller ou adapter valida formato, tipos e limites. O Service Layer valida regras de aplicação e delega invariantes ao domínio.

const schema = z.object({
  orderId: z.string().uuid(),
  actorId: z.string().uuid()
});

Para JSON Schema, consulte Ajv no Node.js.

Invariantes no domínio

class Order {
  approve() {
    if (this.status !== 'pending') {
      throw new InvalidOrderStatusError();
    }
    this.status = 'approved';
  }
}

O Service Layer não deve repetir a regra em cada fluxo.

Autorização

Autenticação pertence ao adapter, mas a decisão “este ator pode executar esta operação” deve estar próxima do caso de uso.

await permissions.assert({
  actorId,
  action: 'order.approve',
  resource: order
});

Consulte RBAC no Node.js e ABAC no Node.js.

Transações

O serviço define a unidade de negócio. Repositories não devem abrir transações independentes quando a operação envolve vários recursos.

await withTransaction(pool, async client => {
  const orders = new PgOrderRepository(client);
  const payments = new PgPaymentRepository(client);

  await applicationService.execute({ orders, payments });
});

Veja Transações PostgreSQL no Node.js.

Não mantenha transação durante HTTP externo

Chamadas a gateways, e-mail ou storage podem demorar. Use estados intermediários, outbox, jobs e idempotência para evitar locks longos.

Outbox

Registre a mudança e o evento na mesma transação:

await tx.orders.save(order);
await tx.outbox.add({
  type: 'order.approved',
  aggregateId: order.id,
  payload: order.toEventPayload()
});

Consulte Outbox Pattern no Node.js.

Idempotência

Operações acionadas por retries precisam impedir repetição de efeitos:

const previous = await idempotency.find(input.idempotencyKey);
if (previous) return previous.result;

Consulte Idempotência em APIs Node.js.

Erros tipados

class OrderNotFoundError extends Error {}
class OrderConflictError extends Error {}
class PermissionDeniedError extends Error {}

O adapter converte esses erros em HTTP, mensagens de fila ou códigos de saída.

Não retorne status HTTP

O Service Layer não deve retornar res.status(409). Ele pode lançar OrderConflictError, enquanto o controller escolhe 409.

Tratamento centralizado

Um middleware mapeia erros:

if (error instanceof OrderNotFoundError) {
  return res.status(404).json({ code: 'ORDER_NOT_FOUND' });
}

Isso mantém controllers pequenos e respostas consistentes.

Reuso em consumidores de fila

consumer.on('message', async message => {
  await approveOrder.execute({
    orderId: message.orderId,
    actorId: message.actorId,
    idempotencyKey: message.eventId
  });
});

A mesma regra é usada sem HTTP.

Reuso em CLI

await approveOrder.execute({
  orderId: argv.order,
  actorId: systemUserId
});

O adapter de linha de comando apenas traduz argumentos.

Dependências explícitas

Use constructor injection:

const approveOrder = new ApproveOrder(
  orderRepository,
  authorizationService,
  unitOfWork
);

Evite importar singletons globais dentro do serviço, porque isso dificulta testes e configuração.

Service Locator

Buscar dependências em um container dentro do método esconde o contrato. Prefira passá-las no construtor.

Tamanho do serviço

Um caso de uso pode ter poucas dezenas de linhas. Se acumular centenas, procure regras que pertencem ao domínio, policies, mappers ou serviços menores.

Orquestração versus coreografia

Em um monólito, o Service Layer pode coordenar diretamente. Em sistemas distribuídos, sagas e eventos podem dividir o fluxo. Consulte Saga Pattern no Node.js.

Timeouts

Dependências externas devem receber prazo. O serviço pode aceitar AbortSignal:

async execute(input, signal: AbortSignal) {
  await provider.call(input, { signal });
}

Não deixe operações penduradas após o cliente desistir.

Retries

Repita apenas falhas transitórias e quando a operação for idempotente. Consulte Retry com Backoff no Node.js.

Feature flags

Flags podem selecionar implementação ou fluxo:

const useNewPricing = await flags.isEnabled(
  'new-pricing',
  { customerId }
);

Consulte Feature Flags no Node.js.

Observabilidade

Nomeie spans e métricas pelo caso de uso:

  • approve_order.duration;
  • approve_order.success_total;
  • approve_order.conflict_total;
  • approve_order.failure_total.

Não use IDs como labels de métrica.

Logs

logger.info({
  operation: 'approve_order',
  orderId,
  actorId
}, 'Pedido aprovado');

Redija dados sensíveis e inclua request ID quando disponível.

Testes unitários

test('aprova pedido pendente', async () => {
  const orders = new InMemoryOrderRepository([
    Order.pending('order-1')
  ]);

  const service = new ApproveOrder(
    orders,
    allowAllAuthorization,
    inMemoryUnitOfWork
  );

  await service.execute({
    orderId: 'order-1',
    actorId: 'user-1'
  });

  const order = await orders.findById('order-1');
  assert.equal(order.status, 'approved');
});

Teste de autorização

Confirme que o repository não é alterado quando o ator não possui permissão.

Teste de rollback

Faça o outbox falhar e confirme que a atualização do pedido também é revertida em integração com banco real.

Teste de idempotência

Execute o mesmo comando duas vezes e confirme que eventos ou cobranças não são duplicados.

Teste de concorrência

Duas aprovações simultâneas devem produzir uma conclusão válida e um conflito controlado, não duas alterações independentes.

Erros comuns

  • Controller gordo: regra continua no adapter.
  • Service genérico: uma classe cresce sem coesão.
  • CRUD puro: intenção do negócio não aparece.
  • HTTP no serviço: reuso fica impossível.
  • Transação no repository: operação composta fica parcial.
  • Singleton global: teste e configuração ficam difíceis.
  • Efeitos externos na transação: locks duram demais.

Boas práticas

  • Modele um serviço por caso de uso.
  • Use entradas e saídas explícitas.
  • Mantenha adapters finos.
  • Delegue invariantes ao domínio.
  • Coordene transações na camada de aplicação.
  • Use repositories por agregado.
  • Trate autorização no caso de uso.
  • Use outbox para eventos.
  • Implemente erros tipados.
  • Teste regras e rollback.

Conclusão

O Service Layer no Node.js oferece uma fronteira clara para os casos de uso. Controllers, filas e CLIs deixam de carregar regras e passam a delegar intenções para serviços independentes do transporte.

Quando cada serviço coordena domínio, autorização, repositories e transações sem conhecer HTTP, a aplicação ganha reutilização e testabilidade. O padrão funciona melhor com nomes orientados ao negócio, dependências explícitas e operações pequenas, evitando tanto controllers gordos quanto classes de serviço gigantes.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita