Skip to content

Slice Structure

Every backend feature in CleanSlice is a self-contained slice -- a NestJS module with a predictable internal layout. This page explains the anatomy of a backend slice and what goes where.

Why Slices Have Internal Layers

A slice is not just a flat folder of files. It mirrors Clean Architecture internally: a domain layer for business logic and contracts, a data layer for implementations, and a dtos folder for API-facing types. This separation makes it possible to change your database, swap an API, or refactor business logic without cascading changes across the codebase.

Slice Anatomy

slices/user/
├── user.prisma                # Prisma model (at slice root)
├── user.module.ts             # NestJS module definition
├── user.controller.ts         # HTTP endpoints
├── user.guard.ts              # Optional auth guard
├── domain/
│   ├── index.ts               # Barrel exports
│   ├── user.types.ts          # Interfaces and enums
│   ├── user.gateway.ts        # Abstract gateway (contract)
│   └── user.service.ts        # Business logic (optional)
├── data/
│   ├── user.gateway.ts        # Concrete gateway (Prisma implementation)
│   └── user.mapper.ts         # Data transformation
└── dtos/
    ├── index.ts               # Barrel exports
    ├── user.dto.ts            # Response DTO
    ├── createUser.dto.ts      # Create request DTO
    ├── updateUser.dto.ts      # Update request DTO
    └── filterUser.dto.ts      # Query params DTO

Layer Responsibilities

LayerFolderContainsDepends On
Presentationroot (*.controller.ts)Controllers, guardsdomain/ and dtos/
Domaindomain/Types, gateway interfaces, servicesNothing external
Datadata/Gateway implementations, mappersdomain/ and Prisma
DTOsdtos/Request/response DTOsdomain/ types

The dependency direction is strict: data/ depends on domain/, but domain/ depends on nothing outside itself. This is dependency inversion in action.

The Module File

The module wires everything together using NestJS dependency injection:

typescript
import { Module } from '@nestjs/common';
import { PrismaModule } from '#/setup/prisma';
import { UserController } from './user.controller';
import { UserService } from './domain/user.service';
import { IUserGateway } from './domain/user.gateway';
import { UserGateway } from './data/user.gateway';
import { UserMapper } from './data/user.mapper';

@Module({
  imports: [PrismaModule],
  controllers: [UserController],
  providers: [
    UserService,
    UserMapper,
    { provide: IUserGateway, useClass: UserGateway },
  ],
  exports: [
    UserService,
    { provide: IUserGateway, useClass: UserGateway },
  ],
})
export class UserModule {}

Key points about the module:

  • Gateway binding uses { provide: IUserGateway, useClass: UserGateway } to connect the abstract class to its concrete implementation.
  • Exports list what other modules can access. Only export what is actually needed externally.
  • Imports only the modules this slice depends on.

The domain/ Folder

This is the heart of the slice. It contains everything that defines what the slice does, without any knowledge of how.

Types (domain/user.types.ts)

All interfaces and enums that define the shape of data:

typescript
export interface IUserData {
  id: string;
  name: string;
  email: string;
  roles: RoleTypes[];
  createdAt?: Date;
  updatedAt?: Date;
}

export interface ICreateUserData {
  name: string;
  email: string;
  roles?: RoleTypes[];
}

export interface IUpdateUserData {
  name?: string;
  roles?: RoleTypes[];
}

Abstract Gateway (domain/user.gateway.ts)

Defines the data access contract as an abstract class:

typescript
export abstract class IUserGateway {
  abstract getUsers(filter?: IUserFilter): Promise<{ data: IUserData[]; meta: IMetaResponse }>;
  abstract getUser(id: string): Promise<IUserData>;
  abstract createUser(data: ICreateUserData): Promise<IUserData>;
  abstract updateUser(id: string, data: IUpdateUserData): Promise<IUserData>;
  abstract deleteUser(id: string): Promise<boolean>;
}

Service (domain/user.service.ts)

Optional business logic layer. See Services for when to use it and when to skip it.

Barrel Export (domain/index.ts)

typescript
export * from './user.types';
export * from './user.gateway';
export * from './user.service';

The data/ Folder

Contains the concrete implementations that the domain layer defines as contracts.

Gateway (data/user.gateway.ts)

Implements the abstract gateway using Prisma:

typescript
@Injectable()
export class UserGateway implements IUserGateway {
  constructor(
    private prisma: PrismaService,
    private map: UserMapper,
  ) {}

  async getUser(id: string): Promise<IUserData> {
    const result = await this.prisma.user.findUnique({ where: { id } });
    return this.map.toData(result);
  }
}

Mapper (data/user.mapper.ts)

Transforms data between Prisma types and domain types:

typescript
@Injectable()
export class UserMapper {
  toData(data: User): IUserData {
    return {
      id: data.id,
      name: data.name,
      email: data.email,
      roles: data.roles as RoleTypes[],
      createdAt: data.createdAt,
      updatedAt: data.updatedAt,
    };
  }
}

The dtos/ Folder

DTOs sit at the API boundary. They define what goes in (request DTOs) and what comes out (response DTOs), with validation and Swagger decorators.

typescript
export class UserDto implements IUserData {
  @ApiProperty() id: string;
  @ApiProperty() name: string;
  @ApiProperty() email: string;
  @ApiProperty({ enum: RoleTypes, isArray: true }) roles: RoleTypes[];
}
typescript
export * from './user.dto';
export * from './createUser.dto';
export * from './updateUser.dto';
export * from './filterUser.dto';

Naming Conventions

ItemFile NameClass NameConvention
Moduleuser.module.tsUserModuleSingular
Controlleruser.controller.tsUserControllerSingular class, plural route
Serviceuser.service.tsUserServiceSingular
Gateway (abstract)user.gateway.tsIUserGatewayI-prefix
Gateway (concrete)user.gateway.tsUserGatewaySingular
Mapperuser.mapper.tsUserMapperSingular
Typesuser.types.tsIUserData etc.I-prefix for interfaces
Response DTOuser.dto.tsUserDtoSingular
Create DTOcreateUser.dto.tsCreateUserDtocamelCase file
Update DTOupdateUser.dto.tsUpdateUserDtocamelCase file
Filter DTOfilterUser.dto.tsFilterUserDtocamelCase file

Submodule Pattern

For infrastructure slices that group related functionality (like AWS services), use a parent module that re-exports submodules:

typescript
@Module({
  imports: [S3Module, CognitoModule, BedrockModule],
  exports: [S3Module, CognitoModule, BedrockModule],
})
export class AwsModule {}
slices/aws/
├── aws.module.ts
├── s3/
│   ├── s3.module.ts
│   └── s3.repository.ts
├── cognito/
│   ├── cognito.module.ts
│   └── cognito.repository.ts
└── bedrock/
    ├── bedrock.module.ts
    └── bedrock.repository.ts

Import Aliases

Use the # path alias for cross-slice imports:

typescript
import { IUserGateway, IUserData } from '#/user/domain';
import { UserDto } from '#/user/dtos';
import { PrismaService } from '#/setup/prisma';

What's Next?

  • Controllers -- The presentation layer in detail
  • Gateways -- Abstract and concrete gateways
  • Services -- When you need a service layer
  • Types -- Domain type conventions

Built with CleanSlice