ProjectCore· 150 min read

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:

  1. The API route (/api/posts) is the backend: it holds the list of posts and returns them as JSON when asked.
  2. The list page (/blog) runs on the server, fetches the posts from that API, and shows each title as a <Link>.
  3. A visitor clicks a title, say "Why I love React". The <Link> takes them to the dynamic post page at /blog/2.
  4. The dynamic route (app/blog/[id]/page.js) reads 2 from the URL, fetches that one post, and renders its title and body.
  5. generateMetadata also 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.

Step 1: an API endpoint at /api/posts returning the blog data
// 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.

Step 2: a server-rendered list page linking to each post
// 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.

Step 3: a dynamic post page that also sets its own metadata
// 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, /blog and /blog/2 all work.
  • Add a root layout with a header and a <Link> back to /blog on 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, …)?

Answer: The square-bracket folder [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

  1. Add a fifth blog post and confirm it shows in the list and opens at its own URL.
  2. Add a "Back to blog" <Link> on each post page.

🏠 Homework

  1. Extend the blog: add an author name to each post in the API and display it on both the list and the post page.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →