Example: SaaS API with All 6 Packages
This guide walks through the example-saas-api project — a minimal NestJS API that uses every nestarc package in a single app.
By the end, you'll have a User CRUD API with:
- Tenant isolation (RLS)
- Standardized response format
- Automatic audit trail
- Feature-flagged endpoints
- Soft-delete with restore
- Paginated list with filters
Prerequisites
- Node.js >= 18
- Docker (for PostgreSQL)
Setup
git clone https://github.com/nestarc/example-saas-api.git
cd example-saas-api
npm install
docker compose up -d
npx prisma db push
npm run start:devServer runs on http://localhost:3000/api.
Project Structure
src/
├── main.ts # Bootstrap
├── app.module.ts # All 6 nestarc modules registered
├── prisma.service.ts # PrismaClient with 3 chained extensions
└── users/
├── users.module.ts
└── users.controller.ts # 5 endpoints using all 6 packagesStep 1: Prisma Extensions
The key to using multiple nestarc packages is chaining Prisma extensions in the correct order:
// prisma.service.ts
this.extended = this
.$extends(createPrismaTenancyExtension(this.tenancyService)) // 1st: RLS
.$extends(createPrismaSoftDeleteExtension({ // 2nd: soft-delete
softDeleteModels: ['User'],
deletedAtField: 'deletedAt',
}))
.$extends(createAuditExtension({ // 3rd: audit
trackedModels: ['User'],
}));Why this order matters:
- Tenancy first — sets
app.current_tenantviaSET LOCAL, which all subsequent queries depend on - Soft-delete second — intercepts
delete()before audit sees it, so the audit log records a soft-delete (not a hard delete) - Audit-log last — captures the final state of every operation after all other extensions have run
Step 2: Module Registration
// app.module.ts
@Module({
imports: [
// Extracts tenant from X-Tenant-Id header
TenancyModule.forRoot({
tenantExtractor: { type: 'header', header: 'x-tenant-id' },
}),
// Wraps all responses in { success, data, error }
SafeResponseModule.register(),
// Tracks who changed what, with before/after diffs
AuditLogModule.forRoot({
prisma: basePrisma,
actorExtractor: (req) => ({
id: req.headers['x-user-id'] ?? null,
type: 'user',
ip: req.ip,
}),
}),
// DB-backed feature flags
FeatureFlagModule.forRoot({
environment: process.env.NODE_ENV ?? 'development',
prisma: basePrisma,
cacheTtlMs: 30_000,
}),
// Pagination module
PaginationModule,
],
})
export class AppModule {}Each module is independent — you can remove any one without affecting the others.
Step 3: The Controller
A single controller demonstrates all 6 packages:
Create (tenancy + audit-log + safe-response)
@Post()
async create(@Body() body: { name: string; email: string }) {
return this.prisma.extended.user.create({
data: { name: body.name, email: body.email },
});
}What happens behind the scenes:
- tenancy — RLS ensures the user is created under the current tenant
- audit-log — automatically records the create with all field values
- safe-response — wraps the result in
{ success: true, data: { ... } }
List (pagination + soft-delete + tenancy)
@Get()
async findAll(@Paginate() query: PaginateQuery) {
return paginate(query, this.prisma.extended.user, {
sortableColumns: ['name', 'email', 'createdAt'],
filterableColumns: { role: ['$eq', '$in'], name: ['$ilike'] },
searchableColumns: ['name', 'email'],
});
}What happens:
- pagination — parses
?page=1&limit=10&sortBy=name:ASCfrom the query string - soft-delete — automatically adds
WHERE deleted_at IS NULLto exclude deleted records - tenancy — RLS ensures only current tenant's records are returned
Delete (soft-delete + audit-log)
@Delete(':id')
async remove(@Param('id') id: string) {
return this.prisma.extended.user.delete({ where: { id } });
}What happens:
- soft-delete — converts
DELETEtoUPDATE SET deleted_at = now() - audit-log — records the soft-delete with the before state
Feature-flagged endpoint
@Get('analytics')
@FeatureFlag('PREMIUM_ANALYTICS')
async analytics() {
const count = await this.prisma.extended.user.count();
return { totalUsers: count };
}Returns 403 Forbidden unless the PREMIUM_ANALYTICS feature flag is enabled for the current tenant.
Try It
# Create a user
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-H "X-Tenant-Id: tenant-1" \
-H "X-User-Id: admin-1" \
-d '{"name": "Alice", "email": "[email protected]"}'
# List users (paginated)
curl "http://localhost:3000/api/users?page=1&limit=10" \
-H "X-Tenant-Id: tenant-1"
# Soft-delete
curl -X DELETE http://localhost:3000/api/users/<id> \
-H "X-Tenant-Id: tenant-1"
# Feature-flagged (will return 403)
curl http://localhost:3000/api/users/analytics \
-H "X-Tenant-Id: tenant-1"What's Not in This Example
This is intentionally minimal. A production app would also have:
- Authentication middleware (JWT, session, etc.)
- Validation (
class-validator+class-transformer) - RLS setup SQL (see tenancy docs)
- Feature flag seeding (create flags via the FeatureFlagService)
- Swagger documentation (
@nestjs/swaggerintegration)
See the Adoption Roadmap for the recommended order to add each package to your own project.