DEV Community

Alex Spinov
Alex Spinov

Posted on

Next.js Has a Free API — Here's How to Build Full-Stack React Apps with Server Components

Next.js 14+ brings React Server Components, Server Actions, and the App Router — enabling full-stack React apps with minimal client JavaScript.

Getting Started

npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
npm run dev
Enter fullscreen mode Exit fullscreen mode

Server Components (Default)

// app/posts/page.tsx — runs on server, zero client JS
async function getPosts() {
  const res = await fetch("https://api.example.com/posts", { next: { revalidate: 60 } });
  return res.json();
}

export default async function PostsPage() {
  const posts = await getPosts();
  return (
    <div>
      <h1>Blog Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Client Components

"use client";
import { useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Server Actions

// app/actions.ts
"use server";

export async function createPost(formData: FormData) {
  const title = formData.get("title") as string;
  const content = formData.get("content") as string;
  await db.post.create({ data: { title, content } });
  revalidatePath("/posts");
}

// app/posts/new/page.tsx
import { createPost } from "../actions";

export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">Create</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Dynamic Routes

// app/posts/[slug]/page.tsx
export default async function PostPage({ params }: { params: { slug: string } }) {
  const post = await db.post.findUnique({ where: { slug: params.slug } });
  if (!post) notFound();
  return <article><h1>{post.title}</h1><p>{post.content}</p></article>;
}

export async function generateStaticParams() {
  const posts = await db.post.findMany({ select: { slug: true } });
  return posts.map(post => ({ slug: post.slug }));
}
Enter fullscreen mode Exit fullscreen mode

Route Handlers (API Routes)

// app/api/posts/route.ts
import { NextResponse } from "next/server";

export async function GET() {
  const posts = await db.post.findMany();
  return NextResponse.json(posts);
}

export async function POST(request: Request) {
  const data = await request.json();
  const post = await db.post.create({ data });
  return NextResponse.json(post, { status: 201 });
}
Enter fullscreen mode Exit fullscreen mode

Middleware

// middleware.ts
import { NextResponse } from "next/server";

export function middleware(request) {
  if (request.nextUrl.pathname.startsWith("/admin")) {
    const token = request.cookies.get("auth-token");
    if (!token) return NextResponse.redirect(new URL("/login", request.url));
  }
  return NextResponse.next();
}

export const config = { matcher: ["/admin/:path*"] };
Enter fullscreen mode Exit fullscreen mode

Need to extract or automate web content at scale? Check out my web scraping tools on Apify — no coding required. Or email me at spinov001@gmail.com for custom solutions.

Top comments (0)