Next.js App Router Tutorial: Build Your First Full-Stack App

Build a small full-stack Next.js app with the App Router: a server-rendered page that reads data from an API route handler you create.

7 min read

This Next.js App Router tutorial builds a tiny full-stack app. One project serves both a page and an API: the home page reads a list of posts from an API endpoint you also write. The page renders on the server, and the endpoint answers plain HTTP requests.

Start by creating a project with create-next-app. If you have not done that yet, run the scaffolder and then return here. The scaffold gives you a runnable app with an app directory and a root layout already in place.

The files you will create

Two files do all the work, and the rest is generated by create-next-app. The folder layout you will end up with looks like this:

texttext
app/
  page.tsx
  api/
    posts/
      route.ts

The page file serves the home route, and the route file serves the API endpoint at /api/posts. The scaffold already generated app/page.tsx and app/layout.tsx; you will replace the page and add the api folder.

Add the API endpoint

A route handler is a file named route.ts that exports an HTTP method function. Create it first so the page has something to read.

typescripttypescript
// app/api/posts/route.ts
export async function GET() {
  const posts = [
    { id: 1, title: 'Welcome to the App Router' },
    { id: 2, title: 'Fetching data on the server' },
  ];
  return Response.json(posts);
}

The GET function returns a JSON list of two posts. In a real app this is where you would query a database, but a hardcoded array keeps the first example small.

The exported function name must match the HTTP method you want to handle, so GET answers GET requests. Visit http://localhost:3000/api/posts after starting the server and the browser shows that JSON directly.

Build the page

Replace app/page.tsx with an async server component that fetches the endpoint and renders the list. An async component can await the request directly, which is the server component way to load data.

App.tsxApp.tsx
// app/page.tsx
export default async function HomePage() {
  const res = await fetch('http://localhost:3000/api/posts');
  const posts = await res.json();
  return <ul>{posts.map((post) => <li key={post.id}>{post.title}</li>)}</ul>;
}

Because this is a server component, the fetch runs on the server and the browser receives finished HTML rather than a loading spinner. The fetch uses the full localhost address because server-side fetch needs an absolute URL, not a bare path. In production you would point this at your real origin or read the data directly inside the component, which is the pattern the fetching guide recommends.

Run it and see the result

Start the dev server:

bashbash
npm run dev

Open http://localhost:3000. The page shows the two post titles as a list, already rendered.

Then open http://localhost:3000/api/posts to see the raw JSON the page consumed. One project now serves a page and an API, which is the full-stack split in miniature.

Next, learn How File-Based Routing Works in the Next.js App Router to understand how folders become URLs, and read Route Handlers in Next.js to go deeper on the API side.

Rune AI

Rune AI

Key Insights

  • Route handlers expose an API from a route.ts file in the app directory.
  • Server components can fetch data on the server and return finished HTML.
  • One project can serve both pages and API endpoints.
  • File paths decide both the page URLs and the API URLs.
  • The fetch in a server component needs an absolute URL.
RunePowered by Rune AI

Frequently Asked Questions

Do I need a separate backend server?

No. A route handler inside the app directory gives you an API in the same project as your pages, which is enough for many small apps.

Why does the page fetch use the full localhost URL?

Server-side fetch needs an absolute URL, not a bare path. In development that is localhost, and in production you use your deployed origin or read the data directly.

Where does the layout file come from?

create-next-app generates app/layout.tsx for you. It is the root layout that wraps every page, and Next.js also creates it automatically if it is missing.

Conclusion

You built a small full-stack app with the App Router: a route handler that serves JSON and a server component that fetches and renders it. The page and the API live in the same project, which is the core idea of full-stack Next.js.