Skip to content
← Back to blog
NestJSAngularSaaSPolishPrzewodnikTutorial

Tworzenie SaaS z NestJS i Angular: Kompletny Przewodnik 2026

Firas Sayah·July 18, 2026·8 min read

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.

Kod na ekranie monitora

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 →

Laptop z kodem na ekranie

// 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

  1. Indeksy bazy danych: Zawsze dodawaj indeksy na kolumnie tenantId
  2. Cache Redis: Cachuj często odczytywane dane w Redis
  3. Lazy loading: Wykorzystuj lazy loading w routingu Angular
  4. Connection pooling: Optymalizuj pulę połączeń TypeORM
  5. 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.

Ekran dashboardu UI

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

F

Firas Sayah

Senior Software Engineer

Full-stack developer with 5+ years building production SaaS applications with NestJS and Angular.