Next.js Integration
Overview
UDL integrates naturally with Next.js, providing type-safe data fetching for both Server Components and static generation. This guide covers setting up UDL with a Next.js App Router project.
Project Setup
1. Create a Next.js Project
npx create-next-app@latest my-app
cd my-app
2. Install Dependencies
npm install universal-data-layer @universal-data-layer/plugin-source-contentful @universal-data-layer/codegen-typed-queries
3. Configure UDL
Create udl.config.ts in your project root:
import { defineConfig } from 'universal-data-layer';
export const config = defineConfig({
plugins: [
{
name: '@universal-data-layer/plugin-source-contentful',
options: {
spaceId: process.env['CONTENTFUL_SPACE_ID'],
accessToken: process.env['CONTENTFUL_ACCESS_TOKEN'],
environment: process.env['CONTENTFUL_ENVIRONMENT'] || 'master',
indexes: ['slug'],
},
},
],
codegen: {
output: './generated',
extensions: ['@universal-data-layer/codegen-typed-queries'],
},
});
4. Environment Variables
Create .env.local:
CONTENTFUL_SPACE_ID=your_space_id
CONTENTFUL_ACCESS_TOKEN=your_delivery_api_token
CONTENTFUL_ENVIRONMENT=master
5. Add Scripts
Update package.json:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"udl": "universal-data-layer",
"udl:dev": "universal-data-layer"
}
}
Running Development
Start two terminals:
npm run udl
npm run dev
The UDL server runs on http://localhost:4000 and Next.js on http://localhost:3000.
Data Fetching
Server Components
Server Components can directly call udl.query():
import { udl } from 'universal-data-layer/client';
import { GetAllProducts } from '@/generated/queries';
export default async function HomePage() {
const [error, products] = await udl.query(GetAllProducts);
if (error) {
throw new Error(`Failed to fetch products: ${error.message}`);
}
return (
<main>
<h1>Products</h1>
<ul>
{products.map((product) => (
<li key={product.slug}>
{product.name} - ${product.price}
</li>
))}
</ul>
</main>
);
}
Using TypedDocumentNode
With typed queries, you get full type inference:
import { udl } from 'universal-data-layer/client';
import { GetProductBySlug } from '@/generated/queries';
import { notFound } from 'next/navigation';
interface PageProps {
params: Promise<{ slug: string }>;
}
export default async function ProductPage({ params }: PageProps) {
const { slug } = await params;
// Variables are type-checked
const [error, product] = await udl.query(GetProductBySlug, {
variables: { slug },
});
if (error || !product) {
notFound();
}
// product is fully typed
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>${product.price}</p>
</main>
);
}
Inline Queries with gql
For ad-hoc queries, use the gql template literal:
import { udl, gql } from 'universal-data-layer/client';
export default async function HomePage() {
const [error, products] = await udl.query<Array<{ name: string; slug: string }>>(gql`
{
allContentfulProducts {
name
slug
}
}
`);
// ...
}
Static Generation
generateStaticParams
Pre-generate pages for all products:
import { udl, gql } from 'universal-data-layer/client';
export async function generateStaticParams() {
const [error, products] = await udl.query<Array<{ slug: string }>>(gql`
{
allContentfulProducts {
slug
}
}
`);
if (error) {
return [];
}
return products.map((product) => ({
slug: product.slug,
}));
}
This generates static pages at build time for each product.
Query Files
Create .graphql files for your queries:
query GetAllProducts {
allContentfulProducts {
name
slug
price
description
image {
... on ContentfulAsset {
file {
url
}
}
}
}
}
query GetProductBySlug($slug: String!) {
contentfulProduct(slug: $slug) {
name
slug
price
description
image {
... on ContentfulAsset {
file {
url
}
}
}
variants {
... on ContentfulVariant {
name
sku
price
inStock
}
}
}
}
After starting the UDL server, these generate TypedDocumentNode exports in generated/queries/index.ts.
TypeScript Configuration
Update tsconfig.json to resolve the generated types:
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}
Then import with:
import { GetAllProducts } from '@/generated/queries';
Error Handling
The UDL client returns a tuple [error, data]:
const [error, products] = await udl.query(GetAllProducts);
if (error) {
// Handle error
console.error('Query failed:', error.message);
return <ErrorComponent message={error.message} />;
}
// Use products safely - TypeScript knows it's not null here
Example Project
See the complete working example at examples/nextjs in the repository.
The example includes:
- Full UDL + Next.js setup
- TypedDocumentNode queries
- Mock Contentful data (no API key needed)
- Product list and detail pages
- Static generation with
generateStaticParams
Next Steps
- Typed Queries - Learn more about TypedDocumentNode generation
- Querying Data - Advanced query patterns
- Production Deployment - Deploy UDL with your app