Tworzenie SaaS z NestJS i Angular: Kompletny Przewodnik 2026
Dlaczego NestJS + Angular do budowania SaaS?
W zeszlym roku pracowalem nad projektem SaaS dla startupu. Trzy miesiace — tyle zabralo zbudowanie samego uwierzytelniania, platnosci i panelu admina. Klient byl coraz bardziej sfrustrowany, a ja nie napisalem jeszcze ani jednej linijki kodu biznesowego. Gdybym zaczal od gotowego boilerplate'u, zaoszczedziłbym te trzy miesiace i kilka siwych wlosow.
Polska jest jednym z największych rynków IT w Europie, z ponad 400 000 programistów. Coraz więcej polskich firm i startupów decyduje się na budowanie produktów SaaS, a kombinacja NestJS + Angular stanowi jedno z najsolidniejszych rozwiązań technologicznych do tego celu.
NestJS to backendowy framework oparty na TypeScript, z modularną architekturą inspirowaną Angularem. Angular, rozwijany przez Google, to najbardziej dojrzały framework frontendowy do budowy aplikacji klasy enterprise. Oba frameworki dzielą TypeScript jako wspólny język, co pozwala na współdzielenie typów i interfejsów między frontendem a backendem. Szczegółowe porównanie z Next.js znajdziesz w naszym porównaniu NestJS + Angular vs Next.js.
Struktura Projektu
Używamy monorepo Nx do organizacji kodu:
saas-projekt/
├── apps/
│ ├── api/ # Backend NestJS
│ │ ├── src/
│ │ │ ├── auth/ # Moduł uwierzytelniania
│ │ │ ├── billing/ # Moduł płatności
│ │ │ ├── tenants/ # Zarządzanie najemcami
│ │ │ └── users/ # Zarządzanie użytkownikami
│ ├── web/ # Frontend Angular
│ │ ├── src/
│ │ │ ├── app/
│ │ │ │ ├── auth/
│ │ │ │ ├── dashboard/
│ │ │ │ └── shared/
├── libs/ # Współdzielone biblioteki
│ ├── shared-types/ # Współdzielone typy TypeScript
│ └── shared-utils/ # Współdzielone narzędzia
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 →Krok 1: Konfiguracja Środowiska
Rozpoczynamy od przygotowania projektu:
# Utwórz workspace Nx
npx create-nx-workspace@latest saas-projekt --preset=apps
# Dodaj wymagane pluginy
npm install @nx/nest @nx/angular
# Wygeneruj aplikacje
nx g @nx/nest:app api
nx g @nx/angular:app web
# Wygeneruj współdzieloną bibliotekę typów
nx g @nx/js:lib shared-types
Skonfiguruj zmienne środowiskowe w pliku .env:
DATABASE_URL=postgresql://postgres:haslo@localhost:5432/saas_db
JWT_SECRET=twoj-super-tajny-klucz
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
APP_URL=http://localhost:4200
Krok 2: System Uwierzytelniania
Uwierzytelnianie JWT to fundament każdej aplikacji SaaS.
Backend: Serwis Uwierzytelniania
// auth.service.ts
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
private readonly tenantsService: TenantsService,
) {}
async rejestracja(dto: RegisterDto) {
const istniejacyUzytkownik = await this.usersService.findByEmail(
dto.email,
);
if (istniejacyUzytkownik) {
throw new ConflictException('Ten adres e-mail jest już zarejestrowany');
}
const hashHasla = await bcrypt.hash(dto.haslo, 12);
const uzytkownik = await this.usersService.create({
email: dto.email,
password: hashHasla,
imie: dto.imie,
nazwisko: dto.nazwisko,
});
// Automatycznie utwórz nowego najemcę
const tenant = await this.tenantsService.create({
nazwa: dto.nazwaFirmy,
wlascicielId: uzytkownik.id,
plan: 'darmowy',
});
return this.generujTokeny(uzytkownik, tenant.id);
}
async logowanie(dto: LoginDto) {
const uzytkownik = await this.usersService.findByEmail(dto.email);
if (
!uzytkownik ||
!(await bcrypt.compare(dto.haslo, uzytkownik.password))
) {
throw new UnauthorizedException('Nieprawidłowy e-mail lub hasło');
}
return this.generujTokeny(uzytkownik, uzytkownik.tenantId);
}
private generujTokeny(uzytkownik: User, tenantId: string) {
const payload = {
sub: uzytkownik.id,
email: uzytkownik.email,
tenantId,
rola: uzytkownik.rola,
};
return {
accessToken: this.jwtService.sign(payload, { expiresIn: '15m' }),
refreshToken: this.jwtService.sign(payload, { expiresIn: '7d' }),
uzytkownik: {
id: uzytkownik.id,
email: uzytkownik.email,
imie: uzytkownik.imie,
},
};
}
}
Frontend: Serwis Angular
// auth.service.ts
@Injectable({ providedIn: 'root' })
export class AuthService {
private readonly http = inject(HttpClient);
private readonly router = inject(Router);
private aktualnyUzytkownik = signal<User | null>(null);
readonly czyZalogowany = computed(() => !!this.aktualnyUzytkownik());
readonly uzytkownik = this.aktualnyUzytkownik.asReadonly();
zaloguj(dane: { email: string; haslo: string }) {
return this.http.post<AuthResponse>('/api/auth/login', dane).pipe(
tap(odpowiedz => {
localStorage.setItem('accessToken', odpowiedz.accessToken);
localStorage.setItem('refreshToken', odpowiedz.refreshToken);
this.aktualnyUzytkownik.set(odpowiedz.uzytkownik);
this.router.navigate(['/panel']);
}),
);
}
wyloguj() {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
this.aktualnyUzytkownik.set(null);
this.router.navigate(['/logowanie']);
}
}
Krok 3: Architektura Multi-Tenant
Izolacja danych między najemcami jest kluczowa w aplikacjach SaaS:
// tenant.middleware.ts
@Injectable()
export class TenantMiddleware implements NestMiddleware {
constructor(
private readonly tenantsService: TenantsService,
private readonly jwtService: JwtService,
private readonly cls: ClsService,
) {}
async use(req: Request, res: Response, next: NextFunction) {
const token = this.wyodrebnijToken(req);
if (token) {
const payload = this.jwtService.verify(token);
const tenant = await this.tenantsService.findById(payload.tenantId);
if (!tenant || !tenant.aktywny) {
throw new ForbiddenException(
'Nieprawidłowy lub dezaktywowany najemca',
);
}
// Zapisz tenantId w kontekście żądania
this.cls.set('tenantId', tenant.id);
this.cls.set('plan', tenant.plan);
}
next();
}
private wyodrebnijToken(req: Request): string | null {
const auth = req.headers.authorization;
return auth?.startsWith('Bearer ') ? auth.slice(7) : null;
}
}
Automatyczne Filtrowanie Zapytań
// tenant.subscriber.ts
@EventSubscriber()
export class TenantSubscriber implements EntitySubscriberInterface {
constructor(
dataSource: DataSource,
private readonly cls: ClsService,
) {
dataSource.subscribers.push(this);
}
beforeInsert(event: InsertEvent<any>) {
const tenantId = this.cls.get('tenantId');
if (tenantId && event.entity) {
event.entity.tenantId = tenantId;
}
}
afterLoad(entity: any) {
const tenantId = this.cls.get('tenantId');
if (tenantId && entity.tenantId && entity.tenantId !== tenantId) {
throw new ForbiddenException('Brak dostępu do tego zasobu');
}
}
}
Krok 4: Integracja Płatności ze Stripe
Więcej szczegółów o integracji Stripe znajdziesz w naszym kompletnym poradniku Stripe Subscriptions z NestJS.
Chcesz to wszystko gotowe? SaaS Starter zawiera uwierzytelnianie, płatności, multi-tenancy i ponad 55 testów. Wypróbuj darmową wersję Lite →
// billing.service.ts
@Injectable()
export class BillingService {
private stripe: Stripe;
constructor(private readonly configService: ConfigService) {
this.stripe = new Stripe(this.configService.get('STRIPE_SECRET_KEY'));
}
async utworzSesjeCheckout(tenantId: string, planId: string) {
const ceny = {
starter: 'price_starter_miesiecznie',
pro: 'price_pro_miesiecznie',
enterprise: 'price_enterprise_miesiecznie',
};
return this.stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
line_items: [{ price: ceny[planId], quantity: 1 }],
success_url: `${this.configService.get('APP_URL')}/platnosc/sukces`,
cancel_url: `${this.configService.get('APP_URL')}/platnosc/anulowano`,
metadata: { tenantId, planId },
});
}
async obsluzWebhook(zdarzenie: Stripe.Event) {
switch (zdarzenie.type) {
case 'customer.subscription.created':
await this.aktywujSubskrypcje(zdarzenie.data.object);
break;
case 'customer.subscription.deleted':
await this.dezaktywujSubskrypcje(zdarzenie.data.object);
break;
case 'invoice.payment_failed':
await this.obsluzNieudanaPlatnosc(zdarzenie.data.object);
break;
}
}
}
Krok 5: Kontrola Dostępu Oparta na Rolach (RBAC)
// role.guard.ts
@Injectable()
export class RoleGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const wymaganeRole = this.reflector.getAllAndOverride<Rola[]>('role', [
context.getHandler(),
context.getClass(),
]);
if (!wymaganeRole) return true;
const { user } = context.switchToHttp().getRequest();
return wymaganeRole.includes(user.rola);
}
}
// Przykład użycia
@Controller('administracja')
@UseGuards(JwtAuthGuard, RoleGuard)
export class AdministracjaController {
@Get('uzytkownicy')
@Role(Rola.ADMIN, Rola.SUPER_ADMIN)
async pobierzUzytkownikow() {
return this.usersService.znajdzWszystkich();
}
@Patch('uzytkownicy/:id/rola')
@Role(Rola.SUPER_ADMIN)
async zmienRole(@Param('id') id: string, @Body() dto: ZmienRoleDto) {
return this.usersService.zmienRole(id, dto.rola);
}
}
Krok 6: Panel Administracyjny Angular
// dashboard.component.ts
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [KartaStatystykComponent, WykresComponent, CurrencyPipe],
template: `
<h1 class="text-2xl font-bold mb-6">Panel Administracyjny</h1>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<app-karta-statystyk
tytul="Miesięczne Przychody"
[wartosc]="statystyki()?.miesiecznePrzychody | currency:'PLN':'symbol':'1.0-0'"
ikona="trending_up"
/>
<app-karta-statystyk
tytul="Aktywni Użytkownicy"
[wartosc]="statystyki()?.aktywniUzytkownicy"
ikona="people"
/>
<app-karta-statystyk
tytul="Współczynnik Konwersji"
[wartosc]="statystyki()?.wspolczynnikKonwersji + '%'"
ikona="analytics"
/>
<app-karta-statystyk
tytul="Współczynnik Rezygnacji"
[wartosc]="statystyki()?.wspolczynnikRezygnacji + '%'"
ikona="warning"
/>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<app-wykres
tytul="Trend Przychodów"
[dane]="danePrzychodow()"
typ="line"
/>
<app-wykres
tytul="Wzrost Użytkowników"
[dane]="daneUzytkownikow()"
typ="bar"
/>
</div>
`,
})
export class DashboardComponent {
private readonly analyticsService = inject(AnalyticsService);
statystyki = toSignal(this.analyticsService.getStatystyki());
danePrzychodow = toSignal(this.analyticsService.getDanePrzychodow());
daneUzytkownikow = toSignal(this.analyticsService.getDaneUzytkownikow());
}
Krok 7: Wdrożenie z Docker
# docker-compose.prod.yml
services:
api:
build:
context: .
dockerfile: apps/api/Dockerfile
target: production
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
restart: unless-stopped
depends_on:
db:
condition: service_healthy
web:
build:
context: .
dockerfile: apps/web/Dockerfile
ports:
- "80:80"
- "443:443"
restart: unless-stopped
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 10s
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
pgdata:
Wskazówki Dotyczące Wydajności
- Indeksy bazy danych: Zawsze dodawaj indeksy na kolumnie
tenantId - Cache Redis: Cachuj często odczytywane dane w Redis
- Lazy loading: Wykorzystuj lazy loading w routingu Angular
- Connection pooling: Optymalizuj pulę połączeń TypeORM
- CDN: Używaj CloudFront lub Cloudflare dla zasobów statycznych
Budować od Zera czy Użyć Boilerplate?
Budowanie aplikacji SaaS od podstaw wymaga 4-6 miesięcy pracy tylko na funkcje bazowe: uwierzytelnianie, płatności, multi-tenancy, panel administracyjny. To czas, który mógłbyś poświęcić na budowanie unikalnych funkcji swojego produktu.
SaaS Starter zawiera wszystkie te funkcje, gotowe do użycia:
- Darmowa wersja Lite do eksploracji podstawowych funkcji
- Pro ($149): Uwierzytelnianie + płatności + podstawowy panel
- Business ($249): Multi-tenancy + RBAC + zaawansowane funkcje
- Enterprise ($399): Wszystkie funkcje + priorytetowe wsparcie
Główne cechy:
- Uwierzytelnianie JWT + OAuth2 (Google, GitHub)
- Integracja Stripe do subskrypcji
- Kompletna architektura multi-tenant
- Kontrola dostępu oparta na rolach
- Panel administracyjny Angular
- Konfiguracja Docker dla produkcji
- Pipeline CI/CD
Podsumowanie: Zacznij Już Dziś
NestJS i Angular tworzą potężną parę do budowania aplikacji SaaS klasy enterprise. TypeScript zapewnia bezpieczeństwo typów, modularna architektura ułatwia skalowanie, a dojrzałe ekosystemy obu frameworków przyspieszają rozwój.
Nie trać czasu na wymyślanie koła na nowo. Odwiedź demo live SaaS Starter, aby zobaczyć go w akcji. Zacznij od darmowej wersji Lite i rozwijaj swój produkt SaaS już dziś.
Cennik:
- Lite (Darmowy) — Poznaj podstawową architekturę
- Pro ($149) — Uwierzytelnianie + płatności + dashboard
- Business ($249) — Multi-tenancy + RBAC + zaawansowane funkcje
- Enterprise ($399) — Wszystkie funkcje + priorytetowe wsparcie
Zobacz cennik | Wypróbuj demo live | Pobierz darmową wersję Lite
Powiązane artykuły: Angular panel admina: budować czy użyć boilerplate? | Tutorial NestJS REST API | Przewodnik wdrożenia na AWS