Data Access Layer Pattern

Auth + Data + DTO in One Place

The Next.js team recommends a Data Access Layer (DAL): every data function checks the user, fetches, and returns a DTO (only safe fields). Then even if a component forgets a check, sensitive data cannot leak.

// lib/dal/orders.ts
import 'server-only';
import { cache } from 'react';
import { auth } from '@/auth';
import { prisma } from '@/lib/prisma';

export const verifySession = cache(async () => {
  const session = await auth();
  if (!session?.user) throw new Error('Unauthorized');
  return session.user;
});

export async function getMyOrder(orderId: string) {
  const user = await verifySession();
  const order = await prisma.order.findUnique({ where: { id: orderId }, include: { items: true } });
  if (!order || (order.userId !== user.id && user.role !== 'admin')) return null;
  return toOrderDto(order);
}

DTO Mapping

Example

The database row has internal fields (cost price, fraud score, payment ids). The DTO exposes only what the page shows:

type OrderRow = {
  id: string; userId: string; status: 'PAID' | 'SHIPPED';
  totalPaise: number; costPaise: number; fraudScore: number;
  razorpayPaymentId: string; createdAt: Date;
  items: { name: string; qty: number; pricePaise: number }[];
};

type OrderDto = { id: string; status: string; total: string; placedOn: string; items: string[] };

const rupees = (paise: number) => '₹' + (paise / 100).toLocaleString('en-IN', { minimumFractionDigits: 2 });

function toOrderDto(o: OrderRow): OrderDto {
  return {
    id: o.id,
    status: o.status,
    total: rupees(o.totalPaise),
    placedOn: o.createdAt.toISOString().slice(0, 10),
    items: o.items.map((i) => i.qty + ' x ' + i.name + ' @ ' + rupees(i.pricePaise)),
  };
}

const row: OrderRow = {
  id: 'ORD-1001', userId: 'u1', status: 'SHIPPED', totalPaise: 1879800, costPaise: 1500000, fraudScore: 0.02,
  razorpayPaymentId: 'pay_N1x2y3', createdAt: new Date('2026-01-15T10:00:00Z'),
  items: [{ name: 'Redmi Note 14', qty: 1, pricePaise: 1799900 }, { name: 'Phone Case', qty: 1, pricePaise: 79900 }],
};
console.log(toOrderDto(row));

Output

{
  id: 'ORD-1001',
  status: 'SHIPPED',
  total: '₹18,798.00',
  placedOn: '2026-01-15',
  items: [ '1 x Redmi Note 14 @ ₹17,999.00', '1 x Phone Case @ ₹799.00' ]
}

Tainting (Extra Safety)

React has an experimental taintUniqueValue / taintObjectReference API that throws if a marked object or value (like an API key or full user row) is ever passed to a Client Component. Enable it with experimental.taint in next.config.ts. The DAL + DTO pattern is the main defence; tainting is a safety net.