Project: A Full-Stack Blog
Bring it all together: build a small blog with an API route, a list page, dynamic post pages and SEO.
What you will learn
- Build a JSON API route for posts
- Fetch and list posts on the server
- Add dynamic post pages with their own metadata
What you will build
A small but complete full-stack blog, using almost every idea from this course: a built-in API route serves the posts, a list page fetches them on the server, dynamic routes show each post, and metadata gives every page good SEO. No external database needed — the API route holds the data.
How the whole flow fits together
Before we write code, here is the journey your data takes, end to end — read this once so each step below makes sense:
- The API route (
/api/posts) is the backend: it holds the list of posts and returns them as JSON when asked. - The list page (
/blog) runs on the server, fetches the posts from that API, and shows each title as a<Link>. - A visitor clicks a title, say "Why I love React". The
<Link>takes them to the dynamic post page at/blog/2. - The dynamic route (
app/blog/[id]/page.js) reads2from the URL, fetches that one post, and renders its title and body. generateMetadataalso runs for that post and sets the browser-tab title to the post title — so every post is SEO-friendly.
Now we build it in three steps, in the same order as that flow.
Step 1 — the API route (your backend)
Create a route handler that returns a list of blog posts as JSON. This is the data source your pages will read from.
// app/api/posts/route.js
const posts = [
{ id: 1, title: 'Hello Next.js', body: 'My first post.' },
{ id: 2, title: 'Why I love React', body: 'Components are great.' },
{ id: 3, title: 'Shipping on Vercel', body: 'Deploys in minutes.' },
];
export async function GET() {
return Response.json(posts);
}Note: Output (visiting /api/posts): [ { "id": 1, "title": "Hello Next.js", "body": "My first post." }, { "id": 2, "title": "Why I love React", "body": "Components are great." }, { "id": 3, "title": "Shipping on Vercel", "body": "Deploys in minutes." } ] Your backend works: this URL serves the post data as JSON.
Step 2 — the blog list page
Make an async Server Component that fetches the posts and shows each title as a <Link> to its detail page.
// app/blog/page.js
import Link from 'next/link';
export const metadata = { title: 'Blog — CodingClave' };
export default async function BlogPage() {
const res = await fetch('http://localhost:3000/api/posts');
const posts = await res.json();
return (
<main>
<h1>Blog</h1>
<ul>
{posts.map((p) => (
<li key={p.id}>
<Link href={'/blog/' + p.id}>{p.title}</Link>
</li>
))}
</ul>
</main>
);
}Note: Output (at /blog): Blog - Hello Next.js (links to /blog/1) - Why I love React (links to /blog/2) - Shipping on Vercel (links to /blog/3) The titles came from your own API, fetched on the server, and each links to a dynamic post page.
Step 3 — the dynamic post page with SEO
Add app/blog/[id]/page.js. It reads the id from the URL, fetches that one post, shows it, and sets its own page title with generateMetadata.
// app/blog/[id]/page.js
async function getPost(id) {
const res = await fetch('http://localhost:3000/api/posts');
const posts = await res.json();
return posts.find((p) => String(p.id) === id);
}
export async function generateMetadata({ params }) {
const post = await getPost(params.id);
return { title: post ? post.title : 'Post not found' };
}
export default async function PostPage({ params }) {
const post = await getPost(params.id);
if (!post) return <p>Post not found.</p>;
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}Note: Output (at /blog/2): Why I love React Components are great. And the browser tab reads "Why I love React" because generateMetadata set the title from the post. Each post gets its own SEO-friendly title.
Your tasks
- Build all three steps and confirm
/api/posts,/blogand/blog/2all work. - Add a root layout with a header and a
<Link>back to/blogon every page. - Add a fourth post to the API and check it appears in the list and opens.
- Style the blog with a CSS Module or Tailwind so it looks finished.
Tip: Build one step at a time and test in the browser after each. A small finished blog beats a big half-working one.
Watch out: In this project the list and post pages fetch from http://localhost:3000/api/posts so the example is clear. In a real app you would read the data directly on the server (or from a database) instead of fetching your own URL — but this version is perfect for learning the flow.
Note: When this works you have built a real full-stack Next.js app: a backend API, server-rendered pages, dynamic routes and per-page SEO. Deploy it to Vercel (previous lesson) and add the live link to your portfolio!
Q. In this blog, which Next.js feature turns one file into a page for every post (/blog/1, /blog/2, …)?
[id] makes a dynamic route, so the single file app/blog/[id]/page.js serves every post by reading params.id. The API route supplies the data; generateMetadata sets each title.✍️ Practice
- Add a fifth blog post and confirm it shows in the list and opens at its own URL.
- Add a "Back to blog"
<Link>on each post page.
🏠 Homework
- Extend the blog: add an author name to each post in the API and display it on both the list and the post page.