Skip to content
← Back to blog
NestJSGDPRPrivacyCompliance

GDPR Compliance in NestJS: Data Export and Deletion

Firas Sayah·April 14, 2026·5 min read

GDPR Is Not Optional

If your SaaS has any European users, GDPR applies. The regulation requires that users can export their data, request deletion, and manage consent. Fines for non-compliance reach 4% of annual revenue. Fortunately, implementing these requirements in NestJS is methodical and well-defined.

Right to Data Export (Article 20)

Users must be able to download all personal data you store about them in a machine-readable format:

@Injectable()
export class DataExportService {
  async exportUserData(userId: string): Promise<UserDataExport> {
    const [user, orders, activities, preferences] = await Promise.all([
      this.userRepo.findOneBy({ id: userId }),
      this.orderRepo.find({ where: { userId } }),
      this.activityRepo.find({ where: { userId } }),
      this.preferenceRepo.findOneBy({ userId }),
    ]);

    return {
      personalInfo: {
        name: user.name,
        email: user.email,
        createdAt: user.createdAt,
      },
      orders: orders.map(o => ({
        id: o.id,
        amount: o.amount,
        date: o.createdAt,
      })),
      activityLog: activities,
      preferences,
      exportedAt: new Date().toISOString(),
    };
  }
}

Expose this through an API endpoint that generates a JSON or ZIP file:

@Get('me/data-export')
@UseGuards(JwtAuthGuard)
async exportMyData(@CurrentUser() user: User, @Res() res: Response) {
  const data = await this.dataExportService.exportUserData(user.id);
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('Content-Disposition', 'attachment; filename="my-data.json"');
  res.send(JSON.stringify(data, null, 2));
}

Right to Deletion (Article 17)

Users can request that all their personal data be deleted. This is more complex because data exists across multiple tables and potentially external services:

@Injectable()
export class DataDeletionService {
  async deleteUserData(userId: string): Promise<DeletionReport> {
    const report: DeletionReport = { deletedEntities: [] };

    // Order matters — delete dependent records first
    await this.activityRepo.delete({ userId });
    report.deletedEntities.push('activities');

    await this.preferenceRepo.delete({ userId });
    report.deletedEntities.push('preferences');

    await this.sessionRepo.delete({ userId });
    report.deletedEntities.push('sessions');

<div class="not-prose my-8 rounded-xl border border-primary-200 dark:border-primary-800 bg-primary-50 dark:bg-primary-900/20 p-6">
<p class="text-base font-semibold text-primary-900 dark:text-primary-100 mb-1">Cloudrix SaaS Starter ships with this pre-configured and tested.</p>
<p class="text-sm text-primary-700 dark:text-primary-300 mb-3">Skip weeks of boilerplate — auth, payments, multi-tenancy, and deployment included out of the box.</p>
<a href="https://demo.cloudrix.io" class="inline-flex items-center text-sm font-medium text-primary-600 dark:text-primary-400 hover:text-primary-500">Try the live demo &rarr;</a>
</div>


    // Anonymize records you must retain for legal reasons
    await this.orderRepo.update(
      { userId },
      { userName: 'DELETED', userEmail: 'deleted@removed.com' },
    );
    report.deletedEntities.push('orders (anonymized)');

    // Delete from external services
    await this.stripeService.deleteCustomer(userId);
    await this.mailchimpService.removeSubscriber(userId);

    // Finally, delete the user record
    await this.userRepo.delete({ id: userId });
    report.deletedEntities.push('user');

    return report;
  }
}

Note that some data cannot be fully deleted. Financial records often have legal retention requirements. In those cases, anonymize the personal fields while keeping the transactional data.

Consent Management

Track what each user has consented to and when:

@Entity()
export class UserConsent {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  userId: string;

  @Column()
  purpose: string; // 'marketing', 'analytics', 'essential'

  @Column()
  granted: boolean;

  @Column()
  grantedAt: Date;

  @Column({ nullable: true })
  revokedAt: Date;

  @Column()
  ipAddress: string;
}

Create middleware that checks consent before processing:

@Injectable()
export class ConsentMiddleware implements NestMiddleware {
  async use(req: Request, res: Response, next: NextFunction) {
    if (req.user) {
      const consent = await this.consentService.getActiveConsents(req.user.id);
      req['consents'] = consent.map(c => c.purpose);
    }
    next();
  }
}

Data Processing Records (Article 30)

Maintain a log of all data processing activities:

@Injectable()
export class ProcessingLogService {
  async log(entry: ProcessingLogEntry) {
    await this.logRepo.save({
      purpose: entry.purpose,
      dataCategories: entry.categories,
      legalBasis: entry.legalBasis,
      retentionPeriod: entry.retention,
      timestamp: new Date(),
    });
  }
}

Automated Data Retention

Set up a scheduled job that automatically purges data past its retention period:

@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
async purgeExpiredData() {
  const cutoff = subMonths(new Date(), 24); // 2-year retention
  await this.activityRepo.delete({ createdAt: LessThan(cutoff) });
  await this.auditLogRepo.delete({ createdAt: LessThan(cutoff) });
  this.logger.log('Expired data purged successfully');
}

Get Started Today

All of these patterns are fully implemented in SaaS Starter. Skip 2-6 weeks of setup.

F

Firas Sayah

Senior Software Engineer

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