Next js

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

 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 *