NestJS en Angular Tutorial in het Nederlands: Je Eerste SaaS App Bouwen
Eerlijk gezegd? Ik heb vier maanden verspild aan het bouwen van authenticatie, Stripe-webhooks en multi-tenancy voordat ik ook maar een regel code voor mijn eigenlijke product had geschreven. Die fout maak ik nooit meer. In deze tutorial laat ik je zien hoe je een SaaS-app bouwt met NestJS en Angular -- en hoe je maanden aan ontwikkeltijd kunt besparen.
Waarom deze tutorial in het Nederlands
Er zijn duizenden tutorials over NestJS en Angular, maar vrijwel allemaal in het Engels. Voor Nederlandse en Vlaamse ontwikkelaars die liever in hun eigen taal leren, is het aanbod beperkt. Deze tutorial vult dat gat.
We bouwen een complete SaaS-applicatie met NestJS als backend en Angular als frontend. Geen vereenvoudigd voorbeeld, maar een echte basis die je kunt gebruiken voor je eigen product. Wil je eerst begrijpen waarom deze combinatie zo goed werkt? Lees ons artikel over waarom Angular + NestJS ideaal is voor SaaS.
Wat we gaan bouwen
Onze SaaS-app bevat de volgende onderdelen:
- Een NestJS REST API met TypeORM en PostgreSQL
- Gebruikersregistratie en inloggen met JWT
- Multi-tenancy: elke organisatie heeft eigen data
- Rolgebaseerde toegangscontrole
- Een Angular-dashboard met routing en state management
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 →Stap 1: Het project opzetten
We gebruiken Nx om backend en frontend in een monorepo te beheren:
npx create-nx-workspace@latest mijn-saas --preset=nest
cd mijn-saas
nx g @nx/angular:app dashboard
Dit geeft ons twee applicaties: api (NestJS) en dashboard (Angular).
Installeer de benodigde packages voor de API:
npm install @nestjs/typeorm typeorm pg
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install class-validator class-transformer bcrypt
npm install -D @types/bcrypt @types/passport-jwt
Stap 2: Database configuratie
Stel de TypeORM-verbinding in:
// app.module.ts
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 5432,
username: process.env.DB_USER || 'postgres',
password: process.env.DB_WACHTWOORD || 'postgres',
database: process.env.DB_NAAM || 'mijn_saas',
autoLoadEntities: true,
synchronize: process.env.NODE_ENV !== 'production',
}),
GebruikersModule,
AuthModule,
OrganisatiesModule,
],
})
export class AppModule {}
Pro-tip: Gebruik nooit
synchronize: truein productie. Gebruik TypeORM-migraties. Ik heb zelf meegemaakt dat een database kolommen verloor door deze fout -- leer van mijn ervaring!
Stap 3: De Gebruiker-entiteit
Definieer de gebruikersentiteit met TypeORM:
// gebruikers/entities/gebruiker.entity.ts
import {
Entity, PrimaryGeneratedColumn, Column, CreateDateColumn,
} from 'typeorm';
@Entity('gebruikers')
export class Gebruiker {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column()
naam: string;
@Column()
wachtwoord: string;
@Column({ default: 'gebruiker' })
rol: string;
@Column()
organisatieId: string;
@CreateDateColumn()
aangemaaktOp: Date;
}
Stap 4: Authenticatie met JWT
De authenticatieservice regelt registratie en inloggen. Voor een uitgebreide gids over JWT-authenticatie en OAuth, raadpleeg ons speciale artikel.
// auth/auth.service.ts
@Injectable()
export class AuthService {
constructor(
private readonly gebruikersService: GebruikersService,
private readonly jwtService: JwtService,
) {}
async registreren(dto: RegistrerenDto) {
const bestaand = await this.gebruikersService
.zoekOpEmail(dto.email);
if (bestaand) {
throw new ConflictException('Dit emailadres is al in gebruik');
}
const wachtwoordHash = await bcrypt.hash(dto.wachtwoord, 12);
const gebruiker = await this.gebruikersService.aanmaken({
...dto,
wachtwoord: wachtwoordHash,
});
const tokens = this.genereerTokens(gebruiker);
return {
gebruiker: this.zonderWachtwoord(gebruiker),
...tokens,
};
}
async inloggen(dto: InloggenDto) {
const gebruiker = await this.gebruikersService
.zoekOpEmail(dto.email);
if (!gebruiker ||
!await bcrypt.compare(dto.wachtwoord, gebruiker.wachtwoord)) {
throw new UnauthorizedException('Ongeldige inloggegevens');
}
const tokens = this.genereerTokens(gebruiker);
return {
gebruiker: this.zonderWachtwoord(gebruiker),
...tokens,
};
}
private genereerTokens(gebruiker: Gebruiker) {
const payload = {
sub: gebruiker.id,
email: gebruiker.email,
organisatieId: gebruiker.organisatieId,
rol: gebruiker.rol,
};
return {
accessToken: this.jwtService.sign(payload, {
expiresIn: '15m',
}),
refreshToken: this.jwtService.sign(payload, {
expiresIn: '7d',
}),
};
}
private zonderWachtwoord(gebruiker: Gebruiker) {
const { wachtwoord, ...resultaat } = gebruiker;
return resultaat;
}
}
De controller stelt de endpoints beschikbaar:
// auth/auth.controller.ts
@Controller('api/auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('registreren')
registreren(@Body() dto: RegistrerenDto) {
return this.authService.registreren(dto);
}
@Post('inloggen')
inloggen(@Body() dto: InloggenDto) {
return this.authService.inloggen(dto);
}
@Post('token-vernieuwen')
@UseGuards(JwtAuthGuard)
tokenVernieuwen(@Req() req) {
return this.authService.genereerTokens(req.user);
}
}
Stap 5: Multi-tenancy
Elke organisatie moet haar eigen data gescheiden zien. We gebruiken een guard die het organisatie-ID uit de JWT haalt. Voor een diepgaande uitleg, bekijk ons artikel over multi-tenant architectuur met NestJS.
// tenancy/organisatie.guard.ts
@Injectable()
export class OrganisatieGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const gebruiker = request.user;
if (!gebruiker?.organisatieId) {
throw new ForbiddenException(
'Geen organisatie gekoppeld aan dit account'
);
}
request.organisatieId = gebruiker.organisatieId;
return true;
}
}
In je services filter je altijd op organisatie:
// projecten/projecten.service.ts
@Injectable()
export class ProjectenService {
constructor(
@InjectRepository(Project)
private readonly repo: Repository<Project>,
) {}
async allesOpvragen(organisatieId: string) {
return this.repo.find({
where: { organisatieId },
order: { aangemaaktOp: 'DESC' },
});
}
async aanmaken(organisatieId: string, dto: ProjectAanmakenDto) {
const project = this.repo.create({
...dto,
organisatieId,
});
return this.repo.save(project);
}
async bijwerken(
organisatieId: string,
id: string,
dto: ProjectBijwerkenDto,
) {
const project = await this.repo.findOne({
where: { id, organisatieId },
});
if (!project) {
throw new NotFoundException('Project niet gevonden');
}
Object.assign(project, dto);
return this.repo.save(project);
}
}
🚀 Wil je weken aan configuratie overslaan? De Cloudrix SaaS Starter bevat JWT-authenticatie, multi-tenancy, Stripe-integratie, RBAC en Docker-deployment -- alles al geconfigureerd en getest. Bekijk de live demo.
Stap 6: Het Angular-dashboard
Aan de frontendzijde maken we een dashboard met beschermde routes. Voor een uitgebreide gids over het bouwen van een admin-dashboard, bekijk ons Angular admin dashboard tutorial.
// app.routes.ts
export const routes: Routes = [
{
path: 'auth',
loadChildren: () => import('./auth/auth.routes')
.then(m => m.AUTH_ROUTES),
},
{
path: 'dashboard',
canActivate: [authGuard],
loadChildren: () => import('./dashboard/dashboard.routes')
.then(m => m.DASHBOARD_ROUTES),
},
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
];
De authenticatiestate beheren we met Angular Signals:
// auth/auth.store.ts
@Injectable({ providedIn: 'root' })
export class AuthStore {
private readonly http = inject(HttpClient);
private readonly router = inject(Router);
readonly gebruiker = signal<Gebruiker | null>(null);
readonly ingelogd = computed(() => this.gebruiker() !== null);
readonly laden = signal(false);
async inloggen(email: string, wachtwoord: string) {
this.laden.set(true);
try {
const respons = await firstValueFrom(
this.http.post<AuthRespons>('/api/auth/inloggen', {
email,
wachtwoord,
}),
);
localStorage.setItem('token', respons.accessToken);
this.gebruiker.set(respons.gebruiker);
this.router.navigate(['/dashboard']);
} finally {
this.laden.set(false);
}
}
uitloggen() {
localStorage.removeItem('token');
this.gebruiker.set(null);
this.router.navigate(['/auth/inloggen']);
}
}
Stap 7: Deployment met Docker
Maak een docker-compose.yml voor productie. Bekijk ook ons uitgebreide Docker Compose guide voor NestJS en Angular.
services:
api:
build:
context: .
dockerfile: apps/api/Dockerfile
environment:
- DATABASE_URL=postgresql://user:wachtwoord@db:5432/saas
- JWT_SECRET=${JWT_SECRET}
ports:
- "3000:3000"
depends_on:
- db
dashboard:
build:
context: .
dockerfile: apps/dashboard/Dockerfile
ports:
- "80:80"
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
environment:
- POSTGRES_DB=saas
- POSTGRES_USER=user
- POSTGRES_PASSWORD=wachtwoord
Voor deployment op AWS met Terraform, bekijk onze AWS deployment gids.
De volgende stap 💡
Deze tutorial geeft je een stevige basis, maar een productie-SaaS vereist nog veel meer: Stripe-integratie voor betalingen, e-mailnotificaties, achtergrondtaken met BullMQ, audit logging, rate limiting, AVG-compliance en uitgebreide tests.
SaaS Starter is een NestJS + Angular boilerplate die dit allemaal kant-en-klaar biedt. De gratis Lite-versie staat op GitHub zodat je direct aan de slag kunt. Wil je alle functionaliteiten in actie zien, inclusief volledig multi-tenancy, Stripe-integratie en een beheerderspaneel? Bekijk de live demo op demo.cloudrix.io.
Ik gebruik het zelf voor mijn eigen projecten en het heeft me letterlijk maanden aan ontwikkeltijd bespaard. Besteed je tijd aan wat jouw product uniek maakt, niet aan het opnieuw bouwen van de basis.
Klaar om te beginnen? Probeer de demo | Bekijk de prijzen -- de Lite-versie is helemaal gratis.