Skip to main content
Refine provides first-class support for Next.js, enabling you to build powerful server-side rendered (SSR) applications. This guide covers both the App Router and Pages Router approaches.

Installation

Install the Next.js router package:
npm install @refinedev/nextjs-router
Or use the CLI to scaffold a new Next.js project:
npm create refine-app@latest -- --preset refine-nextjs my-app

App Router Setup

Basic Configuration

Setup Refine in your app/layout.tsx:
// app/layout.tsx
import { Refine } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";
import routerProvider from "@refinedev/nextjs-router";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Refine
          dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
          routerProvider={routerProvider}
          resources={[
            {
              name: "posts",
              list: "/posts",
              show: "/posts/show/:id",
              create: "/posts/create",
              edit: "/posts/edit/:id",
            },
          ]}
        >
          {children}
        </Refine>
      </body>
    </html>
  );
}

Server Components with Data Fetching

Fetch data in server components:
// app/posts/page.tsx
import dataProvider from "@refinedev/simple-rest";

const API_URL = "https://api.fake-rest.refine.dev";

interface Post {
  id: number;
  title: string;
  content: string;
}

export default async function PostsPage() {
  const { data, total } = await getData();

  return (
    <div>
      <h1>Posts ({total})</h1>
      {data.map((post) => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </div>
      ))}
    </div>
  );
}

async function getData() {
  const response = await dataProvider(API_URL).getList<Post>({
    resource: "posts",
    pagination: { current: 1, pageSize: 10 },
  });

  return {
    data: response.data,
    total: response.total,
  };
}

Client Components with Refine Hooks

Use Refine hooks in client components:
// app/posts/page.tsx
"use client";

import { useTable } from "@refinedev/core";
import Link from "next/link";

interface Post {
  id: number;
  title: string;
  status: string;
}

export default function PostList() {
  const {
    result,
    tableQuery: { isLoading },
  } = useTable<Post>({
    resource: "posts",
  });

  if (isLoading) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {result?.data.map((post) => (
          <li key={post.id}>
            <Link href={`/posts/show/${post.id}`}>
              {post.title}
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

Hybrid Approach: SSR with Client Hydration

Combine server and client rendering:
// app/posts/page.tsx
import { Suspense } from "react";
import dataProvider from "@refinedev/simple-rest";
import { PostListClient } from "./post-list-client";

const API_URL = "https://api.fake-rest.refine.dev";

export default async function PostsPage() {
  const initialData = await getInitialData();

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <PostListClient initialData={initialData} />
    </Suspense>
  );
}

async function getInitialData() {
  const response = await dataProvider(API_URL).getList({
    resource: "posts",
    pagination: { current: 1, pageSize: 10 },
  });

  return response;
}
// app/posts/post-list-client.tsx
"use client";

import { useTable } from "@refinedev/core";

interface Props {
  initialData: any;
}

export function PostListClient({ initialData }: Props) {
  const { result } = useTable({
    resource: "posts",
    queryOptions: {
      initialData,
    },
  });

  return (
    <div>
      {result?.data.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
}

Persisting Table State in SSR

Handle query parameters for table state:
// app/posts/page.tsx
import { parseTableParams } from "@refinedev/nextjs-router";
import dataProvider from "@refinedev/simple-rest";

interface PostListProps {
  searchParams?: Record<string, string>;
}

export default async function PostList({ searchParams }: PostListProps) {
  const tableParams = parseTableParams(searchParams);
  const { data, total } = await getData(tableParams);

  return (
    <div>
      <h1>Posts ({total})</h1>
      {/* Render posts */}
    </div>
  );
}

async function getData(params: any) {
  const response = await dataProvider(API_URL).getList({
    resource: "posts",
    pagination: params.pagination,
    filters: params.filters,
    sorters: params.sorters,
  });

  return {
    data: response.data,
    total: response.total,
  };
}

Pages Router Setup

Basic Configuration

Setup Refine in _app.tsx:
// pages/_app.tsx
import type { AppProps } from "next/app";
import { Refine } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";
import routerProvider from "@refinedev/nextjs-router/pages";

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Refine
      dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
      routerProvider={routerProvider}
      resources={[
        {
          name: "posts",
          list: "/posts",
          show: "/posts/show/:id",
        },
      ]}
    >
      <Component {...pageProps} />
    </Refine>
  );
}

export default MyApp;

SSR with getServerSideProps

// pages/posts/index.tsx
import { GetServerSideProps } from "next";
import { useTable } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";

const API_URL = "https://api.fake-rest.refine.dev";

interface Post {
  id: number;
  title: string;
}

interface Props {
  initialData: any;
}

export default function PostList({ initialData }: Props) {
  const { result } = useTable<Post>({
    queryOptions: {
      initialData,
    },
  });

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {result?.data.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

export const getServerSideProps: GetServerSideProps = async () => {
  const data = await dataProvider(API_URL).getList<Post>({
    resource: "posts",
    pagination: { current: 1, pageSize: 10 },
  });

  return {
    props: {
      initialData: data,
    },
  };
};

Static Generation with getStaticProps

// pages/posts/[id].tsx
import { GetStaticProps, GetStaticPaths } from "next";
import { useShow } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";

const API_URL = "https://api.fake-rest.refine.dev";

interface Props {
  initialData: any;
}

export default function PostShow({ initialData }: Props) {
  const { result } = useShow({
    queryOptions: {
      initialData,
    },
  });

  return (
    <div>
      <h1>{result?.data?.title}</h1>
      <p>{result?.data?.content}</p>
    </div>
  );
}

export const getStaticPaths: GetStaticPaths = async () => {
  const { data } = await dataProvider(API_URL).getList({
    resource: "posts",
    pagination: { current: 1, pageSize: 100 },
  });

  const paths = data.map((post) => ({
    params: { id: post.id.toString() },
  }));

  return {
    paths,
    fallback: "blocking",
  };
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const data = await dataProvider(API_URL).getOne({
    resource: "posts",
    id: params?.id as string,
  });

  return {
    props: {
      initialData: data,
    },
    revalidate: 60, // Revalidate every 60 seconds
  };
};

Persisting Table State in Pages Router

// pages/posts/index.tsx
import { GetServerSideProps } from "next";
import { parseTableParams } from "@refinedev/nextjs-router/pages";
import dataProvider from "@refinedev/simple-rest";

const API_URL = "https://api.fake-rest.refine.dev";

export const getServerSideProps: GetServerSideProps = async ({ resolvedUrl }) => {
  const tableParams = parseTableParams(resolvedUrl?.split("?")[1] ?? "");

  const data = await dataProvider(API_URL).getList({
    resource: "posts",
    ...tableParams,
  });

  return {
    props: {
      initialData: data,
    },
  };
};

export default function PostList({ initialData }: any) {
  const { result } = useTable({
    queryOptions: {
      initialData,
    },
  });

  return <div>{/* Render table */}</div>;
}

Authentication in SSR

App Router Authentication

// app/posts/layout.tsx
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
import { authProvider } from "@providers/auth-provider";

export default async function PostsLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const { authenticated } = await checkAuth();

  if (!authenticated) {
    redirect("/login");
  }

  return <div>{children}</div>;
}

async function checkAuth() {
  const cookieStore = cookies();
  const auth = cookieStore.get("auth");

  return {
    authenticated: !!auth,
  };
}

Pages Router Authentication

// pages/posts/index.tsx
import { GetServerSideProps } from "next";
import { authProvider } from "@providers/auth-provider";

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { authenticated, redirectTo } = await authProvider.check(context);

  if (!authenticated && redirectTo) {
    return {
      redirect: {
        destination: redirectTo,
        permanent: false,
      },
    };
  }

  return {
    props: {},
  };
};

Using Client and Server Providers

Separate providers for client and server:
// providers/data-provider/data-provider.server.ts
import dataProvider from "@refinedev/simple-rest";

export const dataProviderServer = dataProvider(API_URL);
// providers/data-provider/data-provider.client.ts
"use client";

import dataProvider from "@refinedev/simple-rest";

export const dataProviderClient = dataProvider(API_URL);
Since Refine components use React Context, they must be client components. Create separate provider instances for server and client when needed.

Best Practices

  1. Use server components for initial data: Fetch data on the server for better SEO and performance
  2. Hydrate with initial data: Pass server-fetched data to client components via initialData
  3. Handle loading states: Show appropriate loading indicators during client-side navigation
  4. Cache strategically: Use Next.js caching features with revalidate in getStaticProps
  5. Implement error boundaries: Handle errors gracefully in both server and client
  6. Use environment variables: Store API URLs and secrets in environment variables
  7. Optimize images: Use Next.js <Image> component for automatic optimization
  8. Implement pagination: Handle large datasets with proper pagination

Handling 404s

App Router

// app/not-found.tsx
import { Suspense } from "react";

export default function NotFound() {
  return (
    <Suspense>
      <div>
        <h1>404 - Page Not Found</h1>
      </div>
    </Suspense>
  );
}

Pages Router

// pages/404.tsx
export default function Custom404() {
  return <h1>404 - Page Not Found</h1>;
}

Performance Optimization

  1. Use Incremental Static Regeneration (ISR):
export const getStaticProps: GetStaticProps = async () => {
  // ...
  return {
    props: { data },
    revalidate: 60, // Revalidate every 60 seconds
  };
};
  1. Implement partial prerendering: Use fallback: "blocking" in getStaticPaths
  2. Optimize data fetching: Fetch only necessary data
  3. Use React Query caching: Leverage Refine’s built-in caching
  4. Implement code splitting: Use dynamic imports for large components

Further Reading

Build docs developers (and LLMs) love