Build a Next.js 16 App with Server Actions Using Cursor in 25 Minutes

Use Cursor's AI pair programming to create a production-ready Next.js 16 app with Server Actions, form handling, and real-time validation.

Problem: Setting Up Next.js 16 Server Actions Takes Too Long

You want to build a Next.js app with Server Actions but manual setup involves configuring TypeScript, handling form validation, managing loading states, and debugging client/server boundaries - easily 2+ hours of work.

You'll learn:

  • How to use Cursor's Composer to scaffold a complete Next.js 16 app
  • Server Actions implementation with proper error handling
  • AI-assisted debugging of client/server boundary issues

Time: 25 min | Level: Intermediate


Why This Approach Works

Cursor's AI understands Next.js conventions and can generate type-safe Server Actions that follow React 19 patterns. Instead of reading docs and writing boilerplate, you describe what you want and iterate on working code.

What you get:

  • Type-safe Server Actions with Zod validation
  • Proper useFormStatus integration
  • Error boundaries and loading states
  • Production-ready patterns, not tutorials

Prerequisites

# Verify installations
node --version  # Need 20.x or higher
npm --version   # Need 10.x or higher
cursor --version  # Need Cursor 0.42 or higher

If Cursor isn't installed: Download from cursor.com


Solution

Step 1: Initialize Project with Cursor

Open Cursor and press Cmd/Ctrl + I to open Composer:

Prompt to use:

Create a new Next.js 16 app called "task-manager" with:
- App router
- TypeScript
- Tailwind CSS
- Server Actions for creating tasks
- Zod for validation
- SQLite with better-sqlite3

Set up the project structure with proper folders.

What Cursor will do:

  • Run npx create-next-app@latest
  • Install dependencies (zod, better-sqlite3)
  • Create /app/actions/ folder structure
  • Set up basic database schema

Expected output:

✔ Creating task-manager...
✔ Installing dependencies...
✔ Created app/actions/tasks.ts
✔ Created lib/db.ts

If it fails:

  • "Command not found": Cursor needs Terminal access - grant permissions in Settings
  • Dependency errors: Tell Cursor "Fix the peer dependency warnings"

Step 2: Build the Server Action

In Composer, prompt:

Create a Server Action in app/actions/tasks.ts that:
- Validates input with Zod (title: string, priority: "low" | "medium" | "high")
- Inserts into SQLite database
- Returns success/error response
- Uses proper Next.js 16 patterns

Why this works: Cursor knows Next.js conventions and will add 'use server' directive, proper error handling, and type-safe returns.

Generated code preview:

'use server'

import { z } from 'zod';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';

const taskSchema = z.object({
  title: z.string().min(1, 'Title required').max(100),
  priority: z.enum(['low', 'medium', 'high']),
});

export async function createTask(formData: FormData) {
  // Cursor adds validation automatically
  const validated = taskSchema.safeParse({
    title: formData.get('title'),
    priority: formData.get('priority'),
  });

  if (!validated.success) {
    return { error: validated.error.flatten().fieldErrors };
  }

  try {
    // Proper SQLite handling
    db.prepare('INSERT INTO tasks (title, priority) VALUES (?, ?)')
      .run(validated.data.title, validated.data.priority);
    
    revalidatePath('/'); // Next.js 16 cache invalidation
    return { success: true };
  } catch (error) {
    return { error: { _form: ['Database error'] } };
  }
}

What Cursor added:

  • 'use server' directive (required for Server Actions)
  • safeParse instead of parse (better error handling)
  • revalidatePath (updates UI automatically)
  • Proper FormData extraction

Step 3: Create the Form Component

Prompt Cursor:

Create a client component app/components/TaskForm.tsx that:
- Uses the createTask Server Action
- Shows loading state with useFormStatus
- Displays validation errors inline
- Resets form on success

Cursor generates:

'use client'

import { useFormState, useFormStatus } from 'react-dom';
import { createTask } from '@/app/actions/tasks';

function SubmitButton() {
  const { pending } = useFormStatus();
  
  return (
    <button 
      type="submit" 
      disabled={pending}
      className="bg-blue-600 disabled:bg-gray-400"
    >
      {pending ? 'Creating...' : 'Create Task'}
    </button>
  );
}

export function TaskForm() {
  const [state, formAction] = useFormState(createTask, null);

  return (
    <form action={formAction} className="space-y-4">
      <div>
        <input 
          name="title" 
          placeholder="Task title"
          className="border rounded px-3 py-2"
        />
        {state?.error?.title && (
          <p className="text-red-600 text-sm">{state.error.title[0]}</p>
        )}
      </div>

      <select name="priority" className="border rounded px-3 py-2">
        <option value="low">Low</option>
        <option value="medium">Medium</option>
        <option value="high">High</option>
      </select>

      <SubmitButton />
      
      {state?.success && (
        <p className="text-green-600">Task created!</p>
      )}
    </form>
  );
}

Why separate SubmitButton: useFormStatus only works in children of <form>, Cursor knows this pattern.


Step 4: Debug with Cursor Chat

Run the app:

npm run dev

Common issue: Form submits but doesn't reset.

Ask Cursor in Chat (Cmd/Ctrl + L):

The form doesn't reset after successful submission. Fix it.

Cursor will suggest:

// Add useEffect to reset
import { useEffect, useRef } from 'react';

export function TaskForm() {
  const [state, formAction] = useFormState(createTask, null);
  const formRef = useRef<HTMLFormElement>(null);

  useEffect(() => {
    if (state?.success) {
      formRef.current?.reset();
    }
  }, [state?.success]);

  return (
    <form ref={formRef} action={formAction}>
      {/* ... */}
    </form>
  );
}

Why this pattern: Server Actions don't auto-reset forms, you need controlled reset.


Step 5: Add Real-time Validation

Ask Cursor:

Add client-side validation that shows errors as user types, before submission.

Cursor adds:

import { useState } from 'react';

export function TaskForm() {
  const [clientErrors, setClientErrors] = useState<Record<string, string>>({});
  
  const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    if (value.length === 0) {
      setClientErrors(prev => ({ ...prev, title: 'Title required' }));
    } else if (value.length > 100) {
      setClientErrors(prev => ({ ...prev, title: 'Max 100 characters' }));
    } else {
      setClientErrors(prev => {
        const { title, ...rest } = prev;
        return rest;
      });
    }
  };

  return (
    <form action={formAction}>
      <input 
        name="title" 
        onChange={handleTitleChange}
        className="border rounded px-3 py-2"
      />
      {(clientErrors.title || state?.error?.title) && (
        <p className="text-red-600 text-sm">
          {clientErrors.title || state.error.title[0]}
        </p>
      )}
      {/* ... */}
    </form>
  );
}

What changed: Cursor duplicated Zod validation logic to client-side for immediate feedback.


Verification

Test the complete flow:

npm run dev

You should see:

  1. Form renders with Tailwind styling
  2. Type invalid title → error appears instantly
  3. Submit → button shows "Creating..."
  4. Success → "Task created!" message
  5. Form resets automatically

Database check:

# In new terminal
npx better-sqlite3 tasks.db "SELECT * FROM tasks"

Should show your created task with title and priority.


Cursor Power Moves

Multi-file Editing

Press Cmd/Ctrl + Shift + I for Composer with file context:

Update both TaskForm and createTask to add a "due_date" field with proper validation

Cursor edits both files atomically.

Error Fixing

When you see a TypeScript error, select it and press Cmd/Ctrl + K:

Fix this type error

Cursor understands the surrounding context.

Code Explanation

Select any code block, right-click → "Cursor: Explain":

  • Why 'use server' is needed
  • How useFormStatus tracks pending state
  • What revalidatePath does

What You Learned

  • Cursor scaffolds full features faster than manual coding
  • Server Actions need 'use server' and proper error handling
  • useFormStatus must be in form children (common gotcha)
  • Client + server validation prevents bad UX and bad data

Limitations:

  • Cursor sometimes over-engineers (review generated code)
  • Doesn't handle complex database migrations (use Drizzle/Prisma)
  • AI can suggest outdated patterns (verify with Next.js docs)

Troubleshooting Guide

"Server Actions are not enabled"

Fix: Update next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverActions: {
      bodySizeLimit: '2mb',
    },
  },
};

module.exports = nextConfig;

Tell Cursor: "Enable Server Actions in next config"


Form submits twice

Cause: React 18+ Strict Mode calls functions twice in development.

Fix: Normal behavior, won't happen in production. Or disable:

// next.config.js
reactStrictMode: false, // Not recommended

Cursor suggests outdated code

Example: Uses getServerSideProps instead of Server Components.

Fix: Be explicit in prompts:

Use Next.js 16 App Router patterns, not Pages Router

Or add to Cursor Rules (.cursorrules file):

Always use Next.js 16 App Router conventions:
- Server Components by default
- Server Actions for mutations
- No getServerSideProps

Production Checklist

Before deploying:

  • Replace SQLite with PostgreSQL (Cursor: "Migrate to Postgres with Prisma")
  • Add rate limiting to Server Actions
  • Implement proper error logging (Sentry)
  • Add loading skeletons (Cursor: "Add Suspense boundaries")
  • Test with React DevTools Profiler

Ask Cursor:

Add production-ready error handling and rate limiting to all Server Actions

Tested on Next.js 16.0.2, Cursor 0.42.3, Node.js 22.x, macOS Sequoia

Cursor Settings Used:

  • Model: Claude 3.7 Sonnet
  • Composer mode: Agent
  • Context: Medium (4000 lines)