Server-Side Rendering & RSC

Use the NDPA Toolkit on the server: /server subpath, validateDsrSubmission, and React Server Components.

Overview

The NDPA Toolkit ships two distinct surfaces:

  • A client surface — the React components, hooks, and providers exported from @tantainnovative/ndpr-toolkit. These carry "use client" directives and depend on React.
  • A pure-logic surface — validators, generators, scoring, locales, and storage adapters exported from @tantainnovative/ndpr-toolkit/server. Zero React in the import graph.

Use /server for any server-side context: Next.js Route Handlers and Server Components, NestJS controllers, Express middleware, Cloudflare Workers, Edge Functions, or scheduled background jobs. Build-output guard tests assert this entry never carries a "use client" directive and never imports react or react-dom — the RSC-safety contract is structurally enforced.

What's in /server

All 50 exports are pure functions or types — no React, no DOM dependency.

CategoryExports
ValidatorsvalidateConsent, validateDsrSubmission, validateProcessingActivity, validateTransfer, validateProcessingRecord
Generators & exportgeneratePolicyText, assemblePolicy, findUnfilledTokens, exportHTML, exportMarkdown, exportPDF, exportDOCX
Domain helpersformatDSRRequest, assessDPIARisk, calculateBreachSeverity, getComplianceScore, sanitizeInput
LocalesdefaultLocale (en), yorubaLocale, igboLocale, hausaLocale, pidginLocale, mergeLocale
Storage adaptersapiAdapter, memoryAdapter, localStorageAdapter, sessionStorageAdapter, cookieAdapter, composeAdapters
TypesEvery type from the public API plus DsrSubmissionPayload, DsrSubmissionValidationResult

Validating DSR submissions on the server

The most common server-side use case: receiving a payload from <DSRRequestForm onSubmit> and validating it before persisting. validateDsrSubmission mirrors the rules the client form enforces, so client and server stay in sync without you hand-rolling zod or class-validator schemas.

Next.js App Router

// app/api/dsr/route.ts
import { validateDsrSubmission } from '@tantainnovative/ndpr-toolkit/server';
import { dsrStore } from '@/lib/dsr-store';

export async function POST(req: Request) {
  const result = validateDsrSubmission(await req.json(), {
    allowedRequestTypes: ['access', 'erasure', 'rectification', 'objection'],
  });
  if (!result.valid) {
    return Response.json({ errors: result.errors }, { status: 422 });
  }

  // result.data is the typed DsrSubmissionPayload — narrowed and safe to persist.
  await dsrStore.create({
    ...result.data,
    receivedAt: Date.now(),
    status: 'pending',
  });
  return Response.json({ ok: true }, { status: 201 });
}

NestJS controller

// dsr.controller.ts
import { Body, Controller, Post, HttpException, HttpStatus } from '@nestjs/common';
import { validateDsrSubmission } from '@tantainnovative/ndpr-toolkit/server';

@Controller('dsr')
export class DsrController {
  constructor(private readonly dsrService: DsrService) {}

  @Post()
  async submit(@Body() body: unknown) {
    const result = validateDsrSubmission(body);
    if (!result.valid) {
      throw new HttpException(
        { errors: result.errors },
        HttpStatus.UNPROCESSABLE_ENTITY,
      );
    }
    return this.dsrService.create(result.data);
  }
}

Express middleware

// routes/dsr.ts
import { Router } from 'express';
import { validateDsrSubmission } from '@tantainnovative/ndpr-toolkit/server';

export const dsrRouter: Router = Router();

dsrRouter.post('/dsr', async (req, res) => {
  const result = validateDsrSubmission(req.body);
  if (!result.valid) {
    return res.status(422).json({ errors: result.errors });
  }
  await dsrStore.create(result.data);
  res.status(201).json({ ok: true });
});

Skip the identity-verification check (e.g. when the consumer's session already authenticated the user) by passing { requireIdentityVerification: false } as the second argument.

Generating policies on the server

Render a privacy policy server-side (for SSR, scheduled email reports, or PDF generation) without loading any React:

// app/api/privacy-policy/pdf/route.ts
import {
  assemblePolicy,
  exportPDF,
  createDefaultContext,
  findUnfilledTokens,
} from '@tantainnovative/ndpr-toolkit/server';

export async function GET() {
  const ctx = createDefaultContext();
  ctx.org.name = 'Acme Nigeria Ltd';
  ctx.org.privacyEmail = 'privacy@acme.ng';
  ctx.org.website = 'https://acme.ng';

  const sections = assemblePolicy(ctx);
  const policy = {
    id: 'p',
    title: 'Acme Privacy Policy',
    templateId: 'default-business',
    organizationInfo: ctx.org,
    sections,
    variableValues: { orgName: ctx.org.name, /* ... */ },
    effectiveDate: Date.now(),
    lastUpdated: Date.now(),
    version: '1.0',
  };

  // CI-style guard before sending to the wire.
  const missing = findUnfilledTokens(JSON.stringify(sections));
  if (missing.length) {
    throw new Error(`Policy is missing: ${missing.join(', ')}`);
  }

  const blob = await exportPDF(policy);
  return new Response(blob, {
    headers: { 'Content-Type': 'application/pdf' },
  });
}

RSC imports — picking the right entry

FromImportRSC-safe
Server Component / Route Handler/server
Client Component (form, banner, etc.)root @tantainnovative/ndpr-toolkit or feature subpathN/A
Storage adapters in any context/adapters
Types (only)/server (preferred) or feature subpath as type imports✅ when typed

The /core entry exists for back-compat — it includes the React NDPRProvider alongside the pure logic. Prefer /server for new server-side code.

See also