---
title: "Next.js App Router (Server Components)"
section: "frontend/server-side-rendering"
platforms: ["javascript", "nextjs"]
gen: 2
last-updated: "2026-03-25T17:40:00.000Z"
url: "https://docs.amplify.aws/react/frontend/server-side-rendering/nextjs-app-router-server-components/"
---

This Quickstart guide will walk you through how to build a task list application with TypeScript, Next.js **App Router with Server Components**, and React. If you are new to these technologies, we recommend you go through the official [React](https://react.dev/learn/tutorial-tic-tac-toe), [Next.js](https://nextjs.org/docs/app/getting-started), and [TypeScript](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html) tutorials first.

## Build UI

Let's add UI that connects to the backend data and auth resources.

### Configure Amplify Client Side

First, install the Amplify UI component library:

```bash showLineNumbers={false}
npm add @aws-amplify/ui-react
```

Next, create a `components` folder in the root of your project and copy the contents below to a file called `ConfigureAmplify.tsx`.

```ts title="components/ConfigureAmplify.tsx"
// components/ConfigureAmplify.tsx
"use client";

import { Amplify } from "aws-amplify";

import outputs from "@/amplify_outputs.json";

Amplify.configure(outputs, { ssr: true });

export default function ConfigureAmplifyClientSide() {
  return null;
}
```

Update `app/layout.tsx` to import and render `<ConfigureAmplifyClientSide />`. This client component will configure Amplify for client pages in our application.

```ts title="app/layout.tsx"
// app/layout.tsx
import "@aws-amplify/ui-react/styles.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

import ConfigureAmplifyClientSide from "@/components/ConfigureAmplify";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <ConfigureAmplifyClientSide />
        {children}
      </body>
    </html>
  );
}
```

### Configure Amplify Server Side

First, install the Amplify Next.js adapter:

```bash showLineNumbers={false}
npm add @aws-amplify/adapter-nextjs
```

Next, create a `utils/amplify-utils.ts` file from the root of the project and paste the code below.  `runWithAmplifyServerContext`,  `cookiesClient`, and `AuthGetCurrentUserServer` are declared here and will be used to gain access to Amplify assets from the server.

```ts title="utils/amplify-utils.ts"
// utils/amplify-utils.ts
import { cookies } from "next/headers";

import { createServerRunner } from "@aws-amplify/adapter-nextjs";
import { generateServerClientUsingCookies } from "@aws-amplify/adapter-nextjs/api";
import { getCurrentUser } from "aws-amplify/auth/server";

import { type Schema } from "@/amplify/data/resource";
import outputs from "@/amplify_outputs.json";

export const { runWithAmplifyServerContext } = createServerRunner({
  config: outputs,
});

export const cookiesClient = generateServerClientUsingCookies<Schema>({
  config: outputs,
  cookies,
});

export async function AuthGetCurrentUserServer() {
  try {
    const currentUser = await runWithAmplifyServerContext({
      nextServerContext: { cookies },
      operation: (contextSpec) => getCurrentUser(contextSpec),
    });
    return currentUser;
  } catch (error) {
    console.error(error);
  }
}
```

### Add server authentication routes

First, create a client-side `Login` component in the `components` folder that will be wrapped in `withAuthenticator`. If the user is logged in, they will be redirected to the index route; otherwise, the [Amplify UI Authenticator component](https://ui.docs.amplify.aws/react/connected-components/authenticator) will be rendered.

```ts title="components/Login.tsx"
// components/Login.tsx
"use client";

import { withAuthenticator } from "@aws-amplify/ui-react";
import { AuthUser } from "aws-amplify/auth";
import { redirect } from "next/navigation";
import { useEffect } from "react";

function Login({ user }: { user?: AuthUser }) {
  useEffect(() => {
    if (user) {
      redirect("/");
    }
  }, [user]);
  return null;
}

export default withAuthenticator(Login);
```

Next, create a new route under `app/login/page.tsx` to render the `Login` component.

```ts title="app/login/page.tsx"
// app/login/page.tsx

import Login from "@/components/Login";

export default function LoginPage() {
  return <Login />;
}
```

<details><summary>Custom <Authenticator> example</summary>

Some applications require more customization for the `<Authenticator>` component. The following example shows how to add a custom Header to the `<Authenticator>`. For this use, you will not need the `Login` file in the `components` folder, but can do everything through the `page` file in the `app/login/` folder. For more customization options, see [Authenticator Customization](https://ui.docs.amplify.aws/react/connected-components/authenticator/customization).

```ts title="app/login/page.tsx"
// app/login/page.tsx - Custom <Authenticator>

"use client";

import {
  Authenticator,
  Text,
  View,
  useAuthenticator,
} from "@aws-amplify/ui-react";
import { redirect } from "next/navigation";
import { useEffect } from "react";

const components = {
  Header() {
    return (
      <View textAlign="center">
        <Text><span style={{color: "white"}}>Authenticator Header</span></Text>
      </View>
    );
  },
};

function CustomAuthenticator() {
  const { user } = useAuthenticator((context) => [context.user]);

  useEffect(() => {
    if (user) {
      redirect("/");
    }
  }, [user]);

  return <Authenticator components={components} />;
}

export default function Login() {
  return (
    <Authenticator.Provider>
      <CustomAuthenticator />
    </Authenticator.Provider>
  );
}

```

</details>

Finally, create a `Logout` component to be used later.

```ts title="components/Logout.tsx"
// components/Logout.tsx

"use client";

import { signOut } from "aws-amplify/auth";
import { useRouter } from "next/navigation";

export default function Logout() {
  const router = useRouter();

  return (
    <button
      onClick={async () => {
        await signOut();
        router.push("/login");
      }}
      className="px-2 bg-white text-black"
    >
      Sign out
    </button>
  );
}
```

> **Info:** **Note**: If using [Amplify UI Social Providers](https://ui.docs.amplify.aws/react/connected-components/authenticator/configuration#social-providers), set the `callbackUrls` for the `/login` route when [configuring social sign-in for your Gen 2 backend](https://docs.amplify.aws/gen2/build-a-backend/auth/add-social-provider/#configure-social-sign-in-backend), as shown below.
> 
> ```ts title="amplify/auth/resource.ts"
import { defineAuth, secret } from '@aws-amplify/backend';

export const auth = defineAuth({
  loginWith: {
    externalProviders: {
      // ...
      callbackUrls: [
        'http://localhost:3000/login',
        'https://mywebsite.com/login'
      ],
      logoutUrls: ['http://localhost:3000/logout', 'https://mywebsite.com/logout']
    }
  }
});
```

### Add middleware for server-side redirect

Create `middleware.ts` in the root of the project with the contents below.

This middleware runs `fetchAuthSession` wrapped in `runWithAmplifyServerContext` and will redirect to `/login` when a user is not logged in.

```ts title="middleware.ts"
// middleware.ts
import { NextRequest, NextResponse } from "next/server";

import { fetchAuthSession } from "aws-amplify/auth/server";

import { runWithAmplifyServerContext } from "@/utils/amplify-utils";

export async function middleware(request: NextRequest) {
  const response = NextResponse.next();

  const authenticated = await runWithAmplifyServerContext({
    nextServerContext: { request, response },
    operation: async (contextSpec) => {
      try {
        const session = await fetchAuthSession(contextSpec, {});
        return session.tokens !== undefined;
      } catch (error) {
        console.log(error);
        return false;
      }
    },
  });

  if (authenticated) {
    return response;
  }

  return NextResponse.redirect(new URL("/login", request.url));
}

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * - login
     */
    "/((?!api|_next/static|_next/image|favicon.ico|login).*)",
  ],
};
```

Run your application with `npm run dev` and navigate to `http://localhost:3000`. You should now see the authenticator, which is already configured and ready for your first sign-up! Create a new user account, confirm the account through email, and then sign in.

### View list of to-do items

Now, let's display data on our app's frontend.

The code below uses the `cookiesClient` to provide access to the `Todo` model defined in the backend.

Modify your app's home page file, `app/page.tsx`, with the following code:

```ts title="app/page.tsx"
// app/page.tsx

import { cookiesClient } from "@/utils/amplify-utils";

async function App() {
  const { data: todos } = await cookiesClient.models.Todo.list();

  return (
    <>
      <h1>Hello, Amplify 👋</h1>
      <ul>
        {todos && todos.map((todo) => <li key={todo.id}>{todo.content}</li>)}
      </ul>
    </>
  );
}

export default App;
```

Once you save the file and navigate back to `http://localhost:3000`, you should see "Hello, Amplify" with a blank page for now because you have only an empty list of to-dos.

### Create a new to-do item

Let's update the component to have a form for prompting the user for the title for creating a new to-do list item and run the `addTodo` method on form submission. In a production app, the additional fields of the `Todo` model would be added to the form.

After creating a to-do, `revalidatePath` is run to clear the Next.js cache for this route to instantly update the results from the server without a full page reload.

```ts title="app/page.tsx"
// app/page.tsx

import { revalidatePath } from "next/cache";

import { AuthGetCurrentUserServer, cookiesClient } from "@/utils/amplify-utils";

import Logout from "@/components/Logout";

async function App() {
  const user = await AuthGetCurrentUserServer();
  const { data: todos } = await cookiesClient.models.Todo.list();

  async function addTodo(data: FormData) {
    "use server";
    const title = data.get("title") as string;
    await cookiesClient.models.Todo.create({
      content: title,
      done: false,
      priority: "medium",
    });
    revalidatePath("/");
  }

  return (
    <>
      <h1>Hello, Amplify 👋</h1>
      {user && <Logout />}
      <form action={addTodo}>
        <input type="text" name="title" />
        <button type="submit">Add Todo</button>
      </form>

      <ul>
        {todos && todos.map((todo) => <li key={todo.id}>{todo.content}</li>)}
      </ul>
    </>
  );
}

export default App;
```

### Terminate dev server

Go to `localhost` in the browser to make sure you can now log in and create and list to-dos. You can end your development session by shutting down the frontend dev server and cloud sandbox. The sandbox prompts you to delete your backend resources. While you can retain your backend, we recommend deleting all resources so you can start clean again next time.
