This content originally appeared on DEV Community and was authored by Jackson Smith
Gancho: Se o seu BFF tem uma camada de Entity, talvez ele já não seja mais um BFF.
1. Introdução
Se o seu BFF tem uma camada de Entity, talvez ele já não seja mais um BFF.
A arquitetura em camadas é um padrão de design clássico que organiza o código em responsabilidades distintas. No NestJS, essa abordagem é a espinha dorsal do framework, onde:
- Controllers lidam com as requisições HTTP e a comunicação com o cliente.
- Services concentram a lógica de negócio e as interações com outros serviços e APIs externas.
- Repositories cuidam da persistência, manipulando as Entities, que representam o modelo de domínio.
O BFF (Backend for Frontend), no entanto, subverte essa regra. Ele não deve ter um domínio de negócio nem uma camada de persistência própria. Seu papel é apenas orquestrar serviços e expor contratos (DTOs) limpos e otimizados para cada cliente (web, mobile, desktop). Se o seu BFF contém Entities, ORMs ou migrations, ele está fazendo o trabalho errado.
2. O problema comum
Times transformam o BFF num serviço de domínio e isso gera:
- Duplicação de regras que já existem nos microsserviços.
- Acoplamento entre mudanças de domínio e frontends.
- Complexidade (migrations, modelos, testes difíceis).
O resultado: BFF pesado e difícil de evoluir.
3. O papel real do BFF
O BFF não é sobre modelagem de entidades; ele é um adaptador e orquestrador que opera na fronteira entre os serviços de domínio e a interface do cliente.
Suas funções essenciais são:
- Orquestração de chamadas: Em vez de o cliente fazer várias chamadas (uma para autenticação, outra para perfil, outra para feature flags), o BFF centraliza a lógica, faz todas as chamadas necessárias e compõe a resposta em um único lugar.
-
Adaptação de contratos: O BFF traduz os contratos dos serviços de domínio para um formato que seja mais conveniente para o cliente. Por exemplo, ele pode converter um
name
,surname
ephoto_url
vindos de um serviço de perfil em um único objetouser
comfullName
epictureUrl
para a tela do aplicativo. -
Composição de DTOs: Este é o coração do BFF. Ele coleta dados de diferentes fontes, como um serviço de
Pedidos
e um deProdutos
, e os junta para criar umOrderDetailsDto
otimizado para a exibição no cliente, sem implementar as regras de negócio de cada serviço.
4. Por que usar só DTOs?
No BFF, DTO = contrato. Benefícios:
- Leveza: sem regras de domínio, sem ORM.
- Flexibilidade: contratos de resposta diferentes por cliente.
- Segurança: evita vazar modelos internos.
5. Tipos de DTO que um BFF costuma ter
- Controller Input DTO — valida/transforma payload do cliente.
- Service Request/Response DTO — como o BFF fala com microserviços.
- Aggregate / Composition DTO — junção de várias respostas.
- Client Response DTO — contrato final entregue ao cliente.
- Error DTO — padroniza erros para o cliente.
Regra: atravesse fronteiras com DTOs. Use pipes e interceptors do NestJS para validar/serializar automaticamente.
6. Exemplo prático — Login (sem mappers explícitos)
Cenário: cliente envia email
e password
. O BFF:
- chama Auth → recebe
accessToken
,refreshToken
,userId
; - busca Profile do usuário;
- consulta Feature Flags;
- compõe e retorna a resposta para o cliente.
6.1 Estrutura sugerida
src/
auth/
auth.controller.ts
auth.orchestrator.service.ts
dtos/
login-request.dto.ts # Controller Input DTO
auth-service.dto.ts # Service Request/Response DTO (Auth)
profile-service.dto.ts # Service Response DTO (Profile)
flags-service.dto.ts # Service Response DTO (Flags)
login-response.dto.ts # Client Response DTO
api-error.dto.ts # Error DTO
shared/
http/http.module.ts # HttpModule com timeout/retries
main.ts # ValidationPipe global
6.2 Configuração global (Validation + transformação)
No main.ts
do Nest, habilite validação e transformação globalmente:
// main.ts
import { ValidationPipe } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // remove campos não anotados
transform: true, // transforma payloads para classes DTO
forbidNonWhitelisted: false,
})
);
await app.listen(3000);
}
bootstrap();
6.3 DTOs (com comentário sobre o tipo)
// dtos/login-request.dto.ts (Controller Input DTO)
import { IsEmail, IsOptional, IsString, MinLength } from "class-validator";
export class LoginRequestDto {
@IsEmail()
email!: string;
@IsString()
@MinLength(6)
password!: string;
@IsOptional()
@IsString()
deviceId?: string;
}
// dtos/auth-service.dto.ts (Service Request DTO)
import { IsString, IsNumber } from "class-validator";
export class AuthServiceRequestDto {
@IsString()
email!: string;
@IsString()
password!: string;
@IsOptional()
@IsString()
deviceId?: string;
}
// dtos/auth-service.dto.ts (Service Response DTO)
export class AuthServiceResponseDto {
@IsString()
userId!: string;
@IsString()
accessToken!: string;
@IsString()
refreshToken!: string;
@IsNumber()
expiresIn!: number; // segundos
}
// dtos/profile-service.dto.ts (Service Response DTO)
import { IsString, IsUrl, IsOptional } from "class-validator";
export class ProfileServiceResponseDto {
@IsString()
userId!: string;
@IsString()
name!: string;
@IsOptional()
@IsUrl()
pictureUrl?: string;
}
// dtos/flags-service.dto.ts (Service Response DTO)
import { IsObject } from "class-validator";
export class FlagsServiceResponseDto {
@IsObject()
flags!: Record<string, boolean>;
}
// dtos/login-response.dto.ts (Client Response DTO)
import {
IsString,
IsObject,
ValidateNested,
IsOptional,
} from "class-validator";
import { Type, Exclude } from "class-transformer";
class UserDto {
@IsString()
id!: string;
@IsString()
name!: string;
@IsOptional()
@IsString()
pictureUrl?: string;
}
class SessionDto {
@IsString()
accessToken!: string;
@IsString()
refreshToken!: string;
@IsString()
expiresAt!: string; // ISO 8601
}
export class LoginResponseDto {
@Type(() => UserDto)
@ValidateNested()
user!: UserDto;
@Type(() => SessionDto)
@ValidateNested()
session!: SessionDto;
@IsObject()
features!: Record<string, boolean>;
}
// dtos/api-error.dto.ts (Error DTO)
import { IsString, IsOptional } from "class-validator";
export class ApiErrorDto {
@IsString()
code!: string; // EX: AUTH_INVALID_CREDENTIALS
@IsString()
message!: string;
@IsOptional()
details?: unknown;
}
6.4 Orquestração com mapeamento automático
No serviço de orquestração, combine respostas e use plainToInstance
do class-transformer
para transformar o objeto final em LoginResponseDto
. Não é necessário criar funções de mapeamento separadas.
// auth.orchestrator.service.ts
import { Injectable, Logger } from "@nestjs/common";
import { HttpService } from "@nestjs/axios";
import { firstValueFrom, timeout } from "rxjs";
import { ConfigService } from "@nestjs/config";
import { LoginRequestDto } from "./dtos/login-request.dto";
import { AuthServiceResponse } from "./dtos/auth-service.dto";
import { ProfileServiceResponse } from "./dtos/profile-service.dto";
import { FlagsServiceResponse } from "./dtos/flags-service.dto";
import { LoginResponseDto } from "./dtos/login-response.dto";
import { plainToInstance } from "class-transformer";
@Injectable()
export class AuthOrchestratorService {
private readonly logger = new Logger(AuthOrchestratorService.name);
constructor(
private readonly http: HttpService,
private readonly config: ConfigService
) {}
async login(payload: LoginRequestDto): Promise<LoginResponseDto> {
const AUTH_URL = this.config.get<string>("SERVICES_AUTH_URL")!;
const PROFILE_URL = this.config.get<string>("SERVICES_PROFILE_URL")!;
const FLAGS_URL = this.config.get<string>("SERVICES_FLAGS_URL")!;
// 1) Auth
const auth$ = this.http
.post<AuthServiceResponse>(`${AUTH_URL}/v1/login`, {
email: payload.email,
password: payload.password,
deviceId: payload.deviceId,
})
.pipe(timeout({ each: 3000 }));
const auth = (await firstValueFrom(auth$)).data;
// 2) Profile e 3) Flags em paralelo
const profile$ = this.http
.get<ProfileServiceResponse>(`${PROFILE_URL}/v1/users/${auth.userId}`)
.pipe(timeout({ each: 3000 }));
const flags$ = this.http
.get<FlagsServiceResponse>(`${FLAGS_URL}/v1/users/${auth.userId}/flags`)
.pipe(timeout({ each: 3000 }));
const [profileRes, flagsRes] = await Promise.all([
firstValueFrom(profile$),
firstValueFrom(flags$),
]);
// Compondo o objeto final (sem funções de mapeamento separadas)
const expiresAt = new Date(
Date.now() + auth.expiresIn * 1000
).toISOString();
const plain = {
user: {
id: auth.userId,
name: profileRes.data.name,
pictureUrl: profileRes.data.pictureUrl,
},
session: {
accessToken: auth.accessToken,
refreshToken: auth.refreshToken,
expiresAt,
},
features: flagsRes.data.flags ?? {},
};
// Transforme em DTO (class-transformer) — útil se você usar @Expose/@Exclude
return plainToInstance(LoginResponseDto, plain);
}
}
6.5 Controller (validação automática e serialização)
No controller, use ValidationPipe
(global) para transformar LoginRequestDto
automaticamente. Para serializar a resposta com class-transformer
, habilite ClassSerializerInterceptor
ou devolva instâncias DTO (como acima).
// auth.controller.ts
import {
Body,
Controller,
HttpCode,
HttpStatus,
Post,
UseInterceptors,
ClassSerializerInterceptor,
} from "@nestjs/common";
import {
ApiBadRequestResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
} from "@nestjs/swagger";
import { AuthOrchestratorService } from "./auth.orchestrator.service";
import { LoginRequestDto } from "./dtos/login-request.dto";
import { LoginResponseDto } from "./dtos/login-response.dto";
import { ApiErrorDto } from "./dtos/api-error.dto";
@ApiTags("Auth (BFF)")
@Controller("bff/auth")
@UseInterceptors(ClassSerializerInterceptor)
export class AuthController {
constructor(private readonly service: AuthOrchestratorService) {}
@Post("login")
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Login (orquestra Auth + Profile + Flags)" })
@ApiOkResponse({ type: LoginResponseDto })
@ApiBadRequestResponse({ type: ApiErrorDto })
async login(@Body() dto: LoginRequestDto): Promise<LoginResponseDto> {
return this.service.login(dto);
}
}
6.6 Exemplo de request / response
Request
POST /bff/auth/login
{
"email": "ana@exemplo.com",
"password": "minhasenha",
"deviceId": "ios-14-pro-123"
}
Response
{
"user": {
"id": "u_123",
"name": "Ana Silva",
"pictureUrl": "https://cdn.exemplo.com/u_123.png"
},
"session": {
"accessToken": "eyJhbGciOi...",
"refreshToken": "eyJhbGciOi...",
"expiresAt": "2025-09-01T12:34:56.000Z"
},
"features": {
"checkout_new_ui": true,
"beta_reader": false
}
}
Observação: perceba que não há Entities nem Repositories. Apenas DTOs, orquestração e transformação automática.
7. Anti‑padrões (o que evitar)
- Entities/ORM dentro do BFF. Se precisa de migrations, está no lugar errado.
- Regras de domínio no BFF — mantenha no serviço responsável.
- Persistir estado de domínio no BFF. Tokens efêmeros OK; estado durável não.
- Expor DTOs internos dos serviços diretamente ao cliente — sempre adapte.
8. O que pode existir no BFF sem virar domínio
- Cache efêmero (TTL curto) para performance.
- Timeouts / retries / circuit-breaker nas integrações.
- Rate limiting, observabilidade, logs e tracing.
- Formatação/enrich de resposta para o cliente.
Regra de ouro: qualquer estado que descreva o negócio e sobreviva a reinícios deve ir para o serviço de domínio.
9. Testes essenciais
- Unitários: validate/transform (DTOs) e lógica de orquestração básica.
- Contract tests (ex.: Pact) entre BFF e microsserviços.
- E2E focado no contrato com o cliente (payload e erros).
No próximo artigo posso detalhar um guia de testes com exemplos práticos — se lhe interessar, me diga qual abordagem prefere (Pact, Pactflow, sinon/axios mocks, etc.).
10. Conclusão
Um BFF bem feito é leve, previsível e voltado ao cliente. Ele não é um “monólito distribuído” nem um serviço de domínio com outro nome. Ao manter o foco em DTOs, orquestração e composição, você garante que a lógica de negócio viva onde deve — no domínio — enquanto o BFF se mantém como uma ferramenta ágil e flexível para entregar valor de forma rápida e segura aos frontends.
Em resumo, um BFF de sucesso é aquele que conhece o seu lugar: ele não é uma camada de persistência, mas sim a camada de experiência do cliente, projetada para servir e adaptar-se, mantendo a arquitetura de microsserviços limpa e evoluindo de forma independente.
11. Referências
- Relação entre DTO e Entity no NestJS – DEV.to
- Validação no NestJS – Documentação Oficial
- Backend for Frontend (BFF) Pattern – GeeksforGeeks
- Discussão: diferença entre DTO e Entity (Spring Boot) – Reddit
- Data Transfer Object (DTO) – Martin Fowler
- Discussão: DTO vs Entity no Symfony – Reddit
- Discussão: servir Entities vs DTOs – Reddit
This content originally appeared on DEV Community and was authored by Jackson Smith