Frameworks

Next.js Integration

Using Universal Data Layer with Next.js applications

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

Terminal
npx create-next-app@latest my-app
cd my-app

2. Install Dependencies

Terminal
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:

udl.config.ts
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:

.env.local
CONTENTFUL_SPACE_ID=your_space_id
CONTENTFUL_ACCESS_TOKEN=your_delivery_api_token
CONTENTFUL_ENVIRONMENT=master

5. Add Scripts

Update package.json:

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "udl": "universal-data-layer",
    "udl:dev": "universal-data-layer"
  }
}

Running Development

Start two terminals:

Terminal 1 - UDL Server
npm run udl
Terminal 2 - Next.js
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():

app/page.tsx
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:

app/products/[slug]/page.tsx
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:

app/page.tsx
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:

app/products/[slug]/page.tsx
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:

app/queries/products.graphql
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:

tsconfig.json
{
  "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