Создание SaaS с NestJS и Angular: Полное руководство 2026
Вы тоже устали от бесконечных TODO-списков инфраструктурных задач, которые стоят между вами и запуском вашего SaaS-продукта? 🚀 Аутентификация, биллинг, мультитенантность, деплой — каждый из этих модулей может съесть недели работы. А ведь вы ещё не написали ни строчки бизнес-логики.
Я прошёл через это сам, и в этом руководстве расскажу, как построить production-ready SaaS максимально эффективно — и где можно сэкономить месяцы работы.
Почему NestJS + Angular для SaaS?
Выбор технологического стека для SaaS-приложения — это решение, с которым вы будете жить годами. Неправильный выбор означает рефакторинг на пятом месяце, когда уже есть платящие клиенты и каждый час простоя стоит денег.
NestJS и Angular решают эту проблему через архитектурную зрелость. Оба фреймворка были спроектированы для масштабных приложений: модульная система, встроенный dependency injection, строгая типизация через TypeScript. Это не инструменты для прототипов — это инструменты для продуктов, которые будут расти.
💡 NestJS vs Next.js — что выбрать? Прочитайте наше подробное сравнение, чтобы понять, какой фреймворк лучше подходит для вашего SaaS.
Для русскоязычного рынка есть и практическое преимущество: архитектура NestJS очень похожа на Spring Boot, что упрощает переход для Java-разработчиков. Angular широко используется в enterprise-проектах в России и СНГ, поэтому найти опытных Angular-разработчиков проще, чем для менее распространённых фреймворков.
Архитектура проекта
SaaS Starter использует Nx monorepo для управления всеми компонентами:
saas-project/
apps/
api/ # NestJS бэкенд
web/ # Angular фронтенд
worker/ # Обработчик фоновых задач
libs/
shared/ # Общие DTO и интерфейсы
database/ # TypeORM сущности и миграции
auth/ # Модуль аутентификации
billing/ # Интеграция с платёжными системами
tenancy/ # Модуль мультитенантности
infra/
terraform/ # Инфраструктура как код
docker/ # Docker-конфигурации
Монорепозиторий позволяет разделять типы и DTO между фронтендом и бэкендом без дублирования кода.
Cloudrix SaaS Starter ships with this pre-configured and tested.
Skip weeks of boilerplate — auth, payments, multi-tenancy, and deployment included out of the box.
Try the live demo →Аутентификация
Полноценная система аутентификации для SaaS включает JWT-токены, OAuth через социальные сети, двухфакторную аутентификацию (2FA) и управление ролями (RBAC).
JWT-аутентификация
// libs/auth/src/jwt.strategy.ts
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly configService: ConfigService,
private readonly usersService: UsersService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get('JWT_SECRET'),
});
}
async validate(payload: JwtPayload): Promise<User> {
const user = await this.usersService.findById(payload.sub);
if (!user || !user.isActive) {
throw new UnauthorizedException('Пользователь не найден или заблокирован');
}
return user;
}
}
OAuth — социальные сети
// apps/api/src/auth/auth.controller.ts
@Controller('auth')
export class AuthController {
@Get('google')
@UseGuards(AuthGuard('google'))
googleLogin() {}
@Get('google/callback')
@UseGuards(AuthGuard('google'))
async googleCallback(@Req() req: Request): Promise<AuthResponse> {
return this.authService.loginWithOAuth(req.user);
}
@Get('github')
@UseGuards(AuthGuard('github'))
githubLogin() {}
@Get('github/callback')
@UseGuards(AuthGuard('github'))
async githubCallback(@Req() req: Request): Promise<AuthResponse> {
return this.authService.loginWithOAuth(req.user);
}
}
Двухфакторная аутентификация
// libs/auth/src/two-factor.service.ts
@Injectable()
export class TwoFactorService {
generateSecret(user: User): TwoFactorSecret {
const secret = authenticator.generateSecret();
const otpauthUrl = authenticator.keyuri(
user.email,
'SaaS Starter',
secret,
);
return { secret, otpauthUrl };
}
verifyToken(token: string, secret: string): boolean {
return authenticator.verify({ token, secret });
}
}
Управление ролями (RBAC)
// libs/auth/src/guards/roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
// Использование
@Controller('settings')
export class SettingsController {
@Patch('billing')
@Roles(Role.Admin, Role.BillingManager)
@UseGuards(JwtAuthGuard, RolesGuard)
updateBilling(@Body() dto: UpdateBillingDto) {
return this.settingsService.updateBilling(dto);
}
}
Мультитенантность
Мультитенантность — фундамент любого SaaS. SaaS Starter использует изоляцию на уровне PostgreSQL-схем: каждый клиент получает собственную схему в базе данных. Подробнее о паттернах мультитенантности читайте в нашем руководстве по multi-tenancy с TypeORM.
// libs/tenancy/src/tenant.middleware.ts
@Injectable()
export class TenantMiddleware implements NestMiddleware {
constructor(private readonly dataSource: DataSource) {}
async use(req: Request, res: Response, next: NextFunction) {
const tenantId = this.resolveTenant(req);
if (!tenantId) {
throw new BadRequestException('Не указан идентификатор организации');
}
// Переключаемся на схему текущего тенанта
await this.dataSource.query(
`SET search_path TO "tenant_${tenantId}", public`
);
req['tenantId'] = tenantId;
next();
}
private resolveTenant(req: Request): string | null {
// Поддержка через поддомен: company.yourapp.com
const host = req.headers.host;
const subdomain = host?.split('.')[0];
if (subdomain && subdomain !== 'www' && subdomain !== 'api') {
return subdomain;
}
// Или через заголовок запроса
return req.headers['x-tenant-id'] as string;
}
}
Регистрация нового клиента
@Injectable()
export class TenantService {
async provision(dto: CreateTenantDto): Promise<Tenant> {
const tenantId = slugify(dto.companyName);
const schema = `tenant_${tenantId}`;
// Создаём схему
await this.dataSource.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`);
// Применяем миграции
await this.runMigrations(schema);
// Создаём администратора
await this.dataSource.query(`SET search_path TO "${schema}"`);
await this.usersService.create({
email: dto.adminEmail,
password: dto.adminPassword,
role: Role.Admin,
});
// Создаём запись о тенанте в public-схеме
return this.tenantsRepository.save({
id: tenantId,
name: dto.companyName,
schema,
plan: 'trial',
});
}
}
Интеграция с платёжными системами
Stripe для международных платежей
Подробное руководство по интеграции Stripe с подписками доступно отдельно.
// libs/billing/src/stripe.service.ts
@Injectable()
export class StripeService {
private stripe: Stripe;
constructor(private configService: ConfigService) {
this.stripe = new Stripe(configService.get('STRIPE_SECRET_KEY'));
}
async createSubscription(customerId: string, priceId: string) {
return this.stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent'],
});
}
async handleWebhook(payload: Buffer, signature: string) {
const event = this.stripe.webhooks.constructEvent(
payload,
signature,
this.configService.get('STRIPE_WEBHOOK_SECRET'),
);
switch (event.type) {
case 'customer.subscription.updated':
await this.updateSubscriptionStatus(event.data.object);
break;
case 'customer.subscription.deleted':
await this.handleCancellation(event.data.object);
break;
case 'invoice.payment_failed':
await this.handlePaymentFailure(event.data.object);
break;
}
}
}
🔥 Знаете ли вы? Создание аутентификации, биллинга, мультитенантности и деплоя с нуля обойдётся в 8-14 недель и $24,000-$84,000. Читайте полный анализ стоимости. С Cloudrix SaaS Starter всё это готово к использованию с первого дня — попробуйте бесплатное демо! 🎯
Фоновые задачи с BullMQ
Отправка писем, генерация отчётов и обработка вебхуков не должны блокировать HTTP-запросы. BullMQ с Redis обеспечивает надёжную очередь задач.
// apps/worker/src/processors/email.processor.ts
@Processor('emails')
export class EmailProcessor {
private readonly logger = new Logger(EmailProcessor.name);
@Process('welcome')
async sendWelcome(job: Job<{ userId: string; tenantId: string }>) {
this.logger.log(`Отправка приветственного письма: ${job.data.userId}`);
await this.dataSource.query(
`SET search_path TO "tenant_${job.data.tenantId}"`
);
const user = await this.usersService.findById(job.data.userId);
await this.emailService.send({
to: user.email,
subject: 'Добро пожаловать!',
template: 'welcome',
context: {
name: user.displayName,
loginUrl: `https://${job.data.tenantId}.yourapp.com/login`,
},
});
}
@Process('invoice')
async sendInvoice(job: Job<InvoiceJobData>) {
this.logger.log(`Генерация счёта для тенанта: ${job.data.tenantId}`);
const pdf = await this.invoiceService.generatePdf(job.data);
await this.emailService.sendWithAttachment({
to: job.data.email,
subject: `Счёт за ${job.data.period}`,
attachment: pdf,
});
}
}
Angular: Фронтенд
Управление состоянием через Signals
Подробнее об Angular Signals для управления состоянием.
// apps/web/src/app/core/auth.store.ts
@Injectable({ providedIn: 'root' })
export class AuthStore {
private readonly http = inject(HttpClient);
currentUser = signal<User | null>(null);
isAuthenticated = computed(() => !!this.currentUser());
currentTenant = signal<string | null>(null);
async login(credentials: LoginDto): Promise<void> {
const response = await firstValueFrom(
this.http.post<AuthResponse>('/api/auth/login', credentials)
);
localStorage.setItem('token', response.accessToken);
this.currentUser.set(response.user);
this.currentTenant.set(response.tenantId);
}
}
Защита маршрутов
// apps/web/src/app/core/guards/auth.guard.ts
export const authGuard: CanActivateFn = (route, state) => {
const authStore = inject(AuthStore);
const router = inject(Router);
if (authStore.isAuthenticated()) {
return true;
}
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url },
});
};
// Маршруты
export const routes: Routes = [
{ path: 'login', component: LoginComponent },
{
path: 'dashboard',
canActivate: [authGuard],
loadChildren: () => import('./dashboard/routes'),
},
];
Деплой в продакшен
SaaS Starter включает полную конфигурацию Docker и Terraform для AWS:
# Локальная разработка
docker-compose up -d
# Деплой на AWS
cd infra/terraform/environments/production
terraform init
terraform plan
terraform apply
Продакшен-архитектура:
- ECS Fargate — контейнеры NestJS API с автомасштабированием
- S3 + CloudFront — глобальная CDN для Angular-приложения
- RDS PostgreSQL — управляемая база данных с бекапами
- ElastiCache Redis — кеш и очереди BullMQ
- CloudWatch — мониторинг и алертинг
Начните прямо сейчас 🎯
Весь код, описанный в этом руководстве, уже реализован в SaaS Starter. Вам не нужно собирать всё с нуля.
- Откройте demo.cloudrix.io и попробуйте бесплатную версию
- Клонируйте репозиторий и запустите
docker-compose up - За несколько минут у вас будет рабочий каркас SaaS-приложения
Бесплатная lite-версия включает аутентификацию, базовую мультитенантность и UI-компоненты. Профессиональная версия добавляет Terraform-деплой, интеграцию со Stripe, расширенный RBAC, аудит-логирование и систему фоновых задач.
Хотите сравнить с другими boilerplate? Прочитайте наше сравнение Cloudrix vs ShipFast vs Supastarter.
Хватит строить инфраструктуру — стройте продукт. Попробуйте Cloudrix SaaS Starter — начните с бесплатной версии, или получите Pro за единоразовый платёж $249 здесь. Сэкономьте 2-3 месяца и сфокусируйтесь на том, что делает ваш продукт уникальным! 🚀