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:
app/
page.tsx
api/
posts/
route.tsThe 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.
// 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/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:
npm run devOpen 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
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.
Frequently Asked Questions
Do I need a separate backend server?
Why does the page fetch use the full localhost URL?
Where does the layout file come from?
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.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.