Module 7: Forms & Server Actions in Next.js 14 with TypeScript

Module 7: Forms & Server Actions in Next.js 14 with TypeScript

AI Reading

Quick summary of this article

Next.js 14 introduces Server Actions, a cleaner way to handle form submissions directly on the server without needing separate API routes. This module covers building forms with Server Actions, validating input using Zod, and adding optimistic UI feedback with the useTransition hook. You can also connect server actions to databases or external APIs for real backend integration.

  • Server Actions use the <form action={serverFunction}> syntax, where the function is marked with 'use server' and receives form data as a FormData object.
  • Zod validation is applied on the server side using zod.safeParse() to ensure inputs like name, email, and message meet required criteria before processing.
  • The useTransition() hook enables client-side loading states (e.g., "Sending...") and success messages for a smoother user experience.
  • Server actions can integrate with databases (like Prisma) or call external APIs using fetch() directly within the server function.
  • This approach eliminates the need for separate API route handlers, simplifying form logic and keeping code more organized.

 Build and Validate Forms with App Router’s New Action-Based Architecture

Handling forms in Next.js 14 has evolved. Instead of relying entirely on API routes or client state, you can now use Server Actions — a cleaner, declarative way to submit forms directly to the server. In this module, you’ll learn to build modern forms, validate them with Zod, and connect them to real backends.


✅ What Are Server Actions?

Server Actions let you run server code (like DB writes) directly from forms, without setting up separate API routes.

Syntax:

<form action={serverFunction}>
  <input name="email" />
  <button type="submit">Submit</button>
</form>

Your function must be async and explicitly marked:

'use server';

export async function serverFunction(formData: FormData) {
  const email = formData.get('email');
  // do something
}

🧪 Example: Simple Contact Form with Server Action

🔹 src/app/contact/page.tsx

import ContactForm from '@/components/forms/ContactForm';

export default function ContactPage() {
  return (
    <div className="p-8 max-w-xl mx-auto">
      <h1 className="text-2xl font-bold mb-4">Contact Us</h1>
      <ContactForm />
    </div>
  );
}

🔹 src/components/forms/ContactForm.tsx

'use client';

import { submitContactForm } from '@/server/actions/contact';

export default function ContactForm() {
  return (
    <form action={submitContactForm} className="space-y-4">
      <input
        name="name"
        type="text"
        placeholder="Your Name"
        className="border w-full p-2 rounded"
        required
      />
      <input
        name="email"
        type="email"
        placeholder="Your Email"
        className="border w-full p-2 rounded"
        required
      />
      <textarea
        name="message"
        placeholder="Your Message"
        className="border w-full p-2 rounded h-32"
        required
      />
      <button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded">
        Send
      </button>
    </form>
  );
}

🔹 src/server/actions/contact.ts

'use server';

export async function submitContactForm(formData: FormData) {
  const name = formData.get('name');
  const email = formData.get('email');
  const message = formData.get('message');

  console.log('Form submitted:', { name, email, message });

  // Simulate saving to DB or sending an email
}

✅ Validation with Zod

Use Zod to validate form input on the server.

npm install zod

🔹 Updated Server Action with Zod

// src/server/actions/contact.ts
'use server';

import { z } from 'zod';

const ContactSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  message: z.string().min(10),
});

export async function submitContactForm(formData: FormData) {
  const rawData = {
    name: formData.get('name'),
    email: formData.get('email'),
    message: formData.get('message'),
  };

  const result = ContactSchema.safeParse(rawData);

  if (!result.success) {
    throw new Error("Validation failed");
  }

  const { name, email, message } = result.data;

  console.log('Validated:', name, email, message);
}

⚡ Optimistic UI Feedback (Client Side)

To give users instant feedback (like “sending…”), manage a loading state client-side.

🔹 Update ContactForm.tsx

'use client';

import { useTransition, useState } from 'react';
import { submitContactForm } from '@/server/actions/contact';

export default function ContactForm() {
  const [isPending, startTransition] = useTransition();
  const [message, setMessage] = useState('');

  const handleSubmit = (formData: FormData) => {
    startTransition(async () => {
      await submitContactForm(formData);
      setMessage('Message sent successfully!');
    });
  };

  return (
    <form action={handleSubmit} className="space-y-4">
      {/* inputs same as before */}

      <button type="submit" disabled={isPending} className="bg-blue-600 text-white px-4 py-2 rounded">
        {isPending ? "Sending..." : "Send"}
      </button>
      {message && <p className="text-green-600">{message}</p>}
    </form>
  );
}

🔗 Submitting to API / Database (Optional)

You can also call your database (e.g. via Prisma) inside the server action:

// example
import { prisma } from "@/lib/prisma";

await prisma.contact.create({
  data: { name, email, message }
});

Or send to a backend API using fetch() from within your server function.


✅ Summary

You’ve learned how to:

Feature Tool/Example
Server-side form submit <form action={fn}> + use server
Input validation zod.safeParse()
Optimistic feedback useTransition() hook
Backend save Prisma, API, or mock DB inside server action

🔜 Coming Up Next:

In Module 8, we’ll build and connect full Backend APIs in app/api/, use Prisma ORM, and handle full CRUD operations in your Next.js 14 app.

Would you like this as Markdown or publish-ready HTML next?

Leave a Reply

Your email address will not be published. Required fields are marked *