0Pricing

Unleashing Fullstack Power: Getting Started with Next.js 15 for Modern Web Apps

Dive into Next.js 15 and discover how to build powerful fullstack web applications. This introductory guide covers setting up your project, understanding Server Components and Actions, and creating your first fullstack features.

N
Next.js 15 Fullstack Web Apps · 11 min read · 2,277 words

Welcome to CoddyKit, where we empower you to build amazing things! Today, we're kicking off an exciting five-part series on mastering Next.js 15 for fullstack web development. In this first installment, we'll lay the groundwork, helping you get started with Next.js 15 and understand why it's becoming the go-to framework for modern, performant, and scalable fullstack applications.

The Evolution of Web Development and Next.js's Role

The web development landscape is constantly evolving. Gone are the days when a frontend framework simply consumed data from a separate backend API. Today, developers seek a more integrated, efficient, and cohesive approach to building web applications. This is precisely where Next.js shines, especially with its latest iteration, Next.js 15.

Next.js, built on top of React, has always been at the forefront of innovation, initially popularizing server-side rendering (SSR) for React applications. With the introduction of the App Router and React Server Components (RSC), Next.js has transformed into a true fullstack framework, allowing you to build both your frontend UI and your backend logic within a single, unified codebase.

Next.js 15, leveraging the power of React 19, further refines this fullstack experience. It enhances the developer experience, improves performance, and provides a robust architecture for building everything from simple marketing sites to complex, data-intensive applications. For mobile learners like you on CoddyKit, understanding this paradigm shift is crucial for building cutting-edge applications.

What Makes Next.js 15 a Fullstack Powerhouse?

The term "fullstack" implies handling both the client-side (frontend) and server-side (backend) aspects of an application. Next.js 15 achieves this through several key features:

  • React Server Components (RSC): This is perhaps the most revolutionary feature. RSCs allow you to render components entirely on the server, reducing the JavaScript bundle size sent to the client and improving initial page load performance. They can directly access server-side resources like databases or file systems, blurring the lines between frontend and backend.
  • Server Actions: Building on RSCs, Server Actions provide a way to define server-side functions that can be directly called from client-side components (e.g., from a form submission). They simplify data mutations, form handling, and database interactions without needing to create explicit API routes for every operation.
  • Route Handlers (API Routes): For more traditional API endpoints, data fetching, or integration with third-party services, Next.js provides Route Handlers (previously known as API Routes). These allow you to create custom backend API endpoints within your app/api directory, handling HTTP methods like GET, POST, PUT, DELETE.
  • Integrated Data Fetching: Next.js offers various strategies for data fetching, including fetching data directly in Server Components, using React's use hook, or traditional client-side fetching from Route Handlers.
  • Deployment Simplicity: When deployed to platforms like Vercel (the creators of Next.js), your fullstack application is automatically optimized and scaled, handling serverless functions for your backend logic and global CDNs for your static assets.

Setting Up Your First Next.js 15 Fullstack Project

Let's roll up our sleeves and get a new Next.js 15 project up and running. Before we start, ensure you have Node.js (v18.17 or later) installed on your machine.

1. Create a New Project

Open your terminal and run the following command:

npx create-next-app@latest my-fullstack-app

The CLI will prompt you with a series of questions. For this introductory guide, we recommend the following choices:

  • Would you like to use TypeScript? Yes
  • Would you like to use ESLint? Yes
  • Would you like to use Tailwind CSS? No (or Yes if you prefer, it won't impact our fullstack logic)
  • Would you like to use src/ directory? No
  • Would you like to use App Router? (recommended) Yes
  • Would you like to customize the default import alias (@/*)? No

Once the installation is complete, navigate into your new project directory:

cd my-fullstack-app
npm run dev

Your application should now be running on http://localhost:3000.

2. Project Structure Overview (App Router)

With the App Router, your primary focus will be on the app directory. This directory is where you define your routes, layouts, and components, and crucially, where your server components, server actions, and route handlers will reside.

  • app/page.tsx: The root page of your application. By default, this is a Server Component.
  • app/layout.tsx: The root layout shared across all pages. Also a Server Component.
  • app/api/: This directory will house your Route Handlers (API Routes).

Building Your First Fullstack Feature: Server Components and Server Actions

Let's create a simple todo list application to demonstrate the fullstack capabilities. We'll start with a Server Component to display items and then add a Server Action to add new items.

1. Displaying Data with a Server Component

Imagine we have a function that fetches todos from a database (for now, we'll simulate it). You can call this directly in a Server Component.

Modify your app/page.tsx:

// app/page.tsx

interface TodoItem {
  id: number;
  text: string;
  completed: boolean;
}

// This function simulates fetching data from a database
// It runs ONLY on the server.
async function getTodos(): Promise<TodoItem[]> {
  // In a real app, this would be a database query (e.g., Prisma, Drizzle)
  // For now, let's return some mock data after a delay
  await new Promise(resolve => setTimeout(resolve, 1000));
  return [
    { id: 1, text: "Learn Next.js 15", completed: false },
    { id: 2, text: "Build a fullstack app", completed: false },
  ];
}

export default async function HomePage() {
  const todos = await getTodos(); // Data fetched on the server

  return (
    <main style={{ maxWidth: "600px", margin: "50px auto", fontFamily: "sans-serif" }}>
      <h1>My Next.js 15 Fullstack Todos</h1>
      <ul>
        {todos.map(todo => (
          <li key={todo.id} style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
            {todo.text}
          </li>
        ))}
      </ul>
    </main>
  );
}

Notice that getTodos() is an async function directly called within the HomePage Server Component. This code runs exclusively on the server, meaning sensitive logic (like database credentials) remains secure and is never exposed to the client.

2. Adding Data with a Server Action

Now, let's add a form to add new todos using a Server Action. We'll create a separate file for our actions, which is a common practice.

Create a new file: app/actions.ts

// app/actions.ts
"use server"; // This directive marks all exports in this file as Server Actions

import { revalidatePath } from "next/cache";

interface TodoItem {
  id: number;
  text: string;
  completed: boolean;
}

// In a real application, this would be a database. For this example,
// we'll use a simple in-memory array on the server.
let todos: TodoItem[] = [
  { id: 1, text: "Learn Next.js 15", completed: false },
  { id: 2, text: "Build a fullstack app", completed: false },
];
let nextId = 3;

export async function addTodo(formData: FormData) {
  const newTodoText = formData.get("todoText") as string;

  if (newTodoText) {
    todos.push({ id: nextId++, text: newTodoText, completed: false });
    console.log("New todo added on server:", newTodoText);
    revalidatePath("/"); // Invalidate the cache for the root path to show new todos
  }
}

// Export the todos array so the server component can access the current state
export { todos };

Now, update your app/page.tsx to include the form and call the Server Action:

// app/page.tsx (updated)

import { addTodo, todos } from "./actions"; // Import the server action and shared todos

interface TodoItem {
  id: number;
  text: string;
  completed: boolean;
}

// This function simulates fetching data from a database
// It runs ONLY on the server, accessing the shared 'todos' array.
async function getTodos(): Promise<TodoItem[]> {
  // In a real app, this would be a database query.
  // For this example, we directly use the in-memory 'todos' array from actions.ts.
  await new Promise(resolve => setTimeout(resolve, 500)); // Simulate network delay
  return todos; // Return the current state of todos from the server's memory
}

export default async function HomePage() {
  const currentTodos = await getTodos(); // Data fetched on the server

  return (
    <main style={{ maxWidth: "600px", margin: "50px auto", fontFamily: "sans-serif" }}>
      <h1>My Next.js 15 Fullstack Todos</h1>
      <ul style={{ listStyle: "none", padding: "0" }}>
        {currentTodos.map(todo => (
          <li key={todo.id} style={{ padding: "8px 0", borderBottom: "1px solid #eee", textDecoration: todo.completed ? "line-through" : "none" }}>
            {todo.text}
          </li>
        ))}
      </ul>

      <form action={addTodo} style={{ marginTop: "20px", display: "flex", gap: "10px" }}>
        <input type="text" name="todoText" placeholder="Add a new todo" style={{ flexGrow: 1, padding: "10px", border: "1px solid #ccc", borderRadius: "4px" }} />
        <button type="submit" style={{ padding: "10px 15px", backgroundColor: "#0070f3", color: "white", border: "none", borderRadius: "4px", cursor: "pointer" }}>
          Add Todo
        </button>
      </form>
    </main>
  );
}

When you submit the form, the addTodo Server Action is invoked directly on the server. After adding the todo, revalidatePath("/") tells Next.js to re-fetch the data for the root path, ensuring your UI updates with the new todo without a full page reload.

When to Use Route Handlers (API Routes)

While Server Actions are great for form submissions and direct mutations, Route Handlers are still essential for building traditional RESTful APIs, integrating with third-party services, or handling complex authentication flows that might not fit neatly into a direct component interaction.

Example: A Simple GET API Endpoint

Let's create an API endpoint that returns a list of items.

Create a file: app/api/items/route.ts

// app/api/items/route.ts
import { NextResponse } from 'next/server';

const items = [
  { id: 1, name: 'Item A' },
  { id: 2, name: 'Item B' },
  { id: 3, name: 'Item C' },
];

export async function GET() {
  return NextResponse.json(items);
}

Now, you can access this API endpoint at http://localhost:3000/api/items. You can fetch this data from a client component or even from a Server Component if needed.

Consuming the API Route from a Client Component

To demonstrate a client-side interaction, let's create a client component that fetches and displays these items.

Create a file: app/client-items.tsx

// app/client-items.tsx
"use client"; // This directive marks this as a Client Component

import React, { useEffect, useState } from 'react';

interface Item {
  id: number;
  name: string;
}

export default function ClientItems() {
  const [items, setItems] = useState<Item[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchItems() {
      try {
        const response = await fetch('/api/items');
        const data = await response.json();
        setItems(data);
      } catch (error) {
        console.error('Failed to fetch items:', error);
      } finally {
        setLoading(false);
      }
    }
    fetchItems();
  }, []);

  if (loading) return <p>Loading items...</p>;

  return (
    <div style={{ marginTop: "30px" }}>
      <h2>Items Fetched Client-Side</h2>
      <ul style={{ listStyle: "none", padding: "0" }}>
        {items.map(item => (
          <li key={item.id} style={{ padding: "8px 0", borderBottom: "1px solid #eee" }}>
            {item.name}
          </li>
        ))}
      </ul>
    </div>
  );
}

Now, include this client component in your app/page.tsx:

// app/page.tsx (final update)

import { addTodo, todos } from "./actions";
import ClientItems from "./client-items"; // Import the client component

interface TodoItem {
  id: number;
  text: string;
  completed: boolean;
}

// This function simulates fetching data from a database
async function getTodos(): Promise<TodoItem[]> {
  await new Promise(resolve => setTimeout(resolve, 500)); 
  return todos; 
}

export default async function HomePage() {
  const currentTodos = await getTodos(); 

  return (
    <main style={{ maxWidth: "600px", margin: "50px auto", fontFamily: "sans-serif" }}>
      <h1>My Next.js 15 Fullstack Todos</h1>
      <ul style={{ listStyle: "none", padding: "0" }}>
        {currentTodos.map(todo => (
          <li key={todo.id} style={{ padding: "8px 0", borderBottom: "1px solid #eee", textDecoration: todo.completed ? "line-through" : "none" }}>
            {todo.text}
          </li>
        ))}
      </ul>

      <form action={addTodo} style={{ marginTop: "20px", display: "flex", gap: "10px" }}>
        <input type="text" name="todoText" placeholder="Add a new todo" style={{ flexGrow: 1, padding: "10px", border: "1px solid #ccc", borderRadius: "4px" }} />
        <button type="submit" style={{ padding: "10px 15px", backgroundColor: "#0070f3", color: "white", border: "none", borderRadius: "4px", cursor: "pointer" }}>
          Add Todo
        </button>
      </form>

      <ClientItems /> {/* Render the client component */}
    </main>
  );
}

Connecting to a Database (Conceptually)

For a truly persistent fullstack application, you'll need a database. Next.js 15 doesn't dictate which database or ORM you use, giving you flexibility. Popular choices include PostgreSQL (often with Vercel Postgres), MongoDB, MySQL, or SQLite. ORMs like Prisma or Drizzle ORM are excellent for interacting with your database in a type-safe manner.

In a real application, our getTodos function and addTodo Server Action would interact with a database client:

// Example: Using Prisma (conceptual)
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// In getTodos or a similar server function:
async function getTodosFromDB() {
  const todos = await prisma.todo.findMany();
  return todos;
}

// In addTodo Server Action:
export async function addTodoToDB(formData: FormData) {
  const newTodoText = formData.get("todoText") as string;
  if (newTodoText) {
    await prisma.todo.create({ data: { text: newTodoText, completed: false } });
    revalidatePath("/");
  }
}

This illustrates how seamlessly you can integrate database operations directly into your server components and actions, keeping your data fetching and mutation logic close to your UI.

Why Next.js 15 for Your Fullstack Journey?

Next.js 15 offers a compelling set of advantages for fullstack development:

  • Unified Developer Experience: Write frontend and backend code in the same language (TypeScript/JavaScript) and often in the same files, reducing context switching.
  • Performance by Default: Server Components optimize initial load times, and Next.js handles bundling, code splitting, and caching automatically.
  • Scalability: Built for the modern web, it scales effortlessly on serverless platforms, handling traffic spikes with ease.
  • React Ecosystem: Leverage the vast React ecosystem, libraries, and community support.
  • Rich Tooling: Excellent developer tools, fast refresh, and a robust build pipeline.

Conclusion

You've just taken your first steps into building fullstack web applications with Next.js 15! We've covered setting up a project, understanding the core concepts of Server Components and Server Actions, and even touched upon traditional API routes and database integration. The power of Next.js 15 lies in its ability to bring server-side capabilities directly into your React components, streamlining development and boosting performance.

This is just the beginning of your journey. In the next post in this series, we'll dive deeper into Best Practices and Tips for Building Robust Next.js 15 Fullstack Apps, helping you write cleaner, more maintainable, and efficient code. Stay tuned!

ProgrammingTutorialCoddyKit

Enjoyed this article?

Explore more tutorials and insights to level up your coding skills.

Browse All Articles →