DEV Community

Cover image for Full-Stack Development Roadmap from Zero to Hero
Oyekola Abdulqobid Bolaji
Oyekola Abdulqobid Bolaji

Posted on • Originally published at oyetech.vercel.app

Full-Stack Development Roadmap from Zero to Hero

🔐 How to Set Up Authentication in Next.js 15 with Server Actions

Authentication is one of the most important parts of any modern web application.

Whether you’re building a portfolio, SaaS platform, or e-commerce app, you’ll need a way for users to securely sign up, log in, and manage their sessions.

In this guide, we’ll set up authentication in Next.js 15 (App Router) using modern best practices — no external auth libraries required.

We’ll use:

  • TypeScript for type safety
  • Server Actions for form handling
  • Cookies/Sessions for authentication state
  • Tailwind CSS for styling

By the end, you’ll have a fully functional authentication system with:

User Registration

Login & Logout

Protected Routes


❓ Why Authentication Matters

Before we dive into code, let’s understand why authentication is essential for every modern app:

  • Keeps user data secure — protects sensitive information from unauthorized access.
  • Enables personalized experiences — dashboards, user settings, and custom content.
  • Builds trust — users feel safe knowing their data is handled responsibly.
  • Foundation for advanced features — payments, subscriptions, and role-based access all depend on a solid auth system.

1️⃣ Step 1: Setting Up the Next.js Project

Let’s start by creating a new Next.js 15 project configured with TypeScript and Tailwind CSS.

npx create-next-app@latest my-auth-app --typescript --tailwind
cd my-auth-app
Enter fullscreen mode Exit fullscreen mode

Copy the commands above into your terminal — this will give you a fresh Next.js 15 setup with the App Router enabled.


2️⃣ Step 2: Creating the Database Model

For simplicity, we’ll use Prisma with SQLite (you can easily switch to PostgreSQL or MySQL later).

Install Prisma

Run the following commands:

npm install prisma @prisma/client
npx prisma init --datasource-provider sqlite
Enter fullscreen mode Exit fullscreen mode

Update Your Prisma Schema

Open your prisma/schema.prisma file and update it with the following model definition:

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  password  String
  createdAt DateTime @default(now())
}
Enter fullscreen mode Exit fullscreen mode

Then migrate:

npx prisma migrate dev --name init
Enter fullscreen mode Exit fullscreen mode

Don't worry if you want to know more about Prisma, I will cover that in another blog soon


3️⃣ Step 3: Creating the Registration Page

We’ll use Next.js Server Actions for form submissions.

app/register/page.tsx

"use client";

import { useState } from "react";

export default function RegisterPage() {
  const [loading, setLoading] = useState(false);

  async function handleRegister(e: React.FormEvent) {
    e.preventDefault();
    setLoading(true);

    const formData = new FormData(e.currentTarget);

    await fetch("/api/register", {
      method: "POST",
      body: formData,
    });

    setLoading(false);
  }

  return (
    <form onSubmit={handleRegister} className="max-w-sm mx-auto mt-10">
      <input
        type="email"
        name="email"
        placeholder="Email"
        required
        className="w-full mb-3 px-3 py-2 border rounded"
      />
      <input
        type="password"
        name="password"
        placeholder="Password"
        required
        className="w-full mb-3 px-3 py-2 border rounded"
      />
      <button
        type="submit"
        disabled={loading}
        className="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700"
      >
        {loading ? "Registering..." : "Register"}
      </button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

4️⃣ Step 4: Creating the API Route for Registration

app/api/register/route.ts

import { NextResponse } from "next/server";
import bcrypt from "bcrypt";
import { prisma } from "@/lib/prisma";

export async function POST(req: Request) {
  const formData = await req.formData();
  const email = formData.get("email") as string;
  const password = formData.get("password") as string;

  const hashedPassword = await bcrypt.hash(password, 10);

  await prisma.user.create({
    data: { email, password: hashedPassword },
  });

  return NextResponse.json({ success: true });
}

Enter fullscreen mode Exit fullscreen mode

5️⃣ Step 5: Login & Session Handling

We’ll use cookies to keep users logged in.

**app/api/login/route.ts**

import { NextResponse } from "next/server";
import bcrypt from "bcrypt";
import prisma from "@/lib/prisma";
import { cookies } from "next/headers";

export async function POST(req: Request) {
  const formData = await req.formData();
  const email = formData.get("email") as string;
  const password = formData.get("password") as string;

  const user = await prisma.user.findUnique({ where: { email } });
  if (!user) return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });

  const valid = await bcrypt.compare(password, user.password);
  if (!valid) return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });

  // Await the cookies() call
  const cookiesStore = await cookies();
  cookiesStore.set("user", JSON.stringify({ id: user.id, email: user.email }), {
    httpOnly: true,
    path: "/",
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax"
  });

  return NextResponse.json({ success: true });
}
Enter fullscreen mode Exit fullscreen mode

6️⃣ Step 6: Protecting Routes

We can read cookies inside server components:

import { cookies } from "next/headers";
import Link from "next/link";

export default async function DashboardPage() {
  const cookiesStore = await cookies()
  const user = cookiesStore.get("user");

  if (!user) {
    return (

        You must log in to view this page.
        Go to Login

    );
  }

  return (

      Welcome back!
      {JSON.parse(user.value).email}

  );
}
Enter fullscreen mode Exit fullscreen mode

7️⃣ Step 7: Logout

We can read cookies inside server components:

// app/api/logout/route.ts
import { NextResponse } from "next/server";
import { cookies } from "next/headers";

export async function POST() {
  const cookiesStore = await cookies()
  cookiesStore.delete("user");
  return NextResponse.json({ success: true });
}
Enter fullscreen mode Exit fullscreen mode

🏁 Wrapping Up

And that’s it 🎉 — you’ve just built a simple authentication system in Next.js 15 using the App Router.

Through this project, you’ve learned:

  • ⚡ How powerful Server Actions and API Routes are in Next.js 15
  • 🧠 How TypeScript makes authentication logic safer and easier to maintain
  • 🎨 How Tailwind CSS helps you build clean, responsive forms quickly
  • ☁️ How seamless Vercel deployments are with modern Next.js setups

This setup is perfect for learning and small-scale projects.

💡 For production-grade apps, consider using NextAuth.js (now Auth.js) — it provides ready-made solutions for:

  • OAuth (Google, GitHub, etc.)
  • JWTs and sessions
  • Advanced security features

With this foundation in place, you’re well on your way to building secure, scalable full-stack applications with Next.js 15.


Read the Full Article

This is a summary of my comprehensive guide. Read the full article with code examples and project ideas on my blog:

👉 How to Set Up Authentication in Next.js 15 with Server Actions

More tutorials on my blog:

Connect with me:


Questions?

What's your biggest challenge in learning full-stack development? Drop a comment below!

Tags: #webdev #nextjs #authentication #tutorial

Top comments (0)