Skip to main content

Next.js Form Submission

Introduction

Form submission is a fundamental part of web applications, enabling users to send data to servers for processing. In Next.js, there are several ways to handle form submissions, ranging from traditional HTML forms to modern React-based approaches with client-side and server-side processing options.

This guide will walk you through different methods of handling form submissions in Next.js applications, from basic approaches to more advanced techniques with proper validation and error handling.

Understanding Form Submission in Next.js

Next.js, being a React framework, provides multiple ways to handle forms:

  1. Client-side form handling with React state
  2. Server-side form processing with API routes
  3. Server Actions (available in Next.js 13.4+)
  4. Form libraries integration (like Formik, React Hook Form)

Let's explore each of these approaches with practical examples.

Basic Form Submission with React State

The simplest approach is to handle forms using React state on the client side.

Example: Client-side Form Handling

jsx
'use client';
import { useState } from 'react';

export default function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitResult, setSubmitResult] = useState(null);

const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};

const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);

try {
// Send data to an API endpoint
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});

const data = await response.json();

if (!response.ok) {
throw new Error(data.message || 'Something went wrong');
}

setSubmitResult({
status: 'success',
message: 'Thank you for your message!'
});

// Reset form
setFormData({ name: '', email: '', message: '' });

} catch (error) {
setSubmitResult({
status: 'error',
message: error.message
});
} finally {
setIsSubmitting(false);
}
};

return (
<div className="form-container">
<h2>Contact Us</h2>

{submitResult && (
<div className={`alert alert-${submitResult.status}`}>
{submitResult.message}
</div>
)}

<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="name">Name</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
/>
</div>

<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>

<div className="form-group">
<label htmlFor="message">Message</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
rows="4"
required
></textarea>
</div>

<button
type="submit"
disabled={isSubmitting}
className="submit-button"
>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</form>
</div>
);
}

In this example:

  • We use React's useState hook to manage form data and submission states
  • The form data is sent to a Next.js API route (/api/contact) when the form is submitted
  • We handle loading states and provide feedback to the user after submission

API Route for Form Processing

To handle the form submission on the server side, we need to create an API route.

Example: Creating a Contact Form API Route

Create a file at app/api/contact/route.js (Next.js 13+):

jsx
import { NextResponse } from 'next/server';

export async function POST(request) {
try {
const data = await request.json();
const { name, email, message } = data;

// Validate form inputs
if (!name || !email || !message) {
return NextResponse.json(
{ message: 'Missing required fields' },
{ status: 400 }
);
}

// Process the form data
// This could be sending an email, storing in a database, etc.
console.log('Form submission received:', data);

// Example: Send an email (you'd need to set up an email service)
// await sendEmail({
// to: '[email protected]',
// subject: 'New Contact Form Submission',
// body: `Name: ${name}\nEmail: ${email}\nMessage: ${message}`
// });

return NextResponse.json({
message: 'Form submission successful'
});

} catch (error) {
console.error('Form submission error:', error);
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
);
}
}

Form Submission with Server Actions (Next.js 13.4+)

Server Actions are a new feature in Next.js that allows you to define server-side functions that can be called directly from client components, making form handling more straightforward.

Example: Using Server Actions

First, make sure you have enabled Server Actions in your next.config.js:

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

module.exports = nextConfig;

Now create a form with Server Actions:

jsx
// app/contact/page.js
'use client';
import { useState } from 'react';

// Server action (defined in the same file for simplicity)
async function submitContactForm(formData) {
'use server';

const name = formData.get('name');
const email = formData.get('email');
const message = formData.get('message');

// Validation
if (!name || !email || !message) {
return {
error: 'All fields are required'
};
}

try {
// Process the form data - save to DB, send email, etc.
console.log('Processing form submission:', { name, email, message });

// Return success response
return { success: 'Thank you for your message!' };
} catch (error) {
return { error: 'There was an error processing your request.' };
}
}

export default function ContactPage() {
const [result, setResult] = useState(null);
const [isPending, setIsPending] = useState(false);

const handleSubmit = async (event) => {
event.preventDefault();
setIsPending(true);

const formData = new FormData(event.target);
const response = await submitContactForm(formData);

setResult(response);
setIsPending(false);

if (response.success) {
// Reset form
event.target.reset();
}
};

return (
<div className="contact-page">
<h1>Contact Us</h1>

{result?.success && (
<div className="success-message">{result.success}</div>
)}

{result?.error && (
<div className="error-message">{result.error}</div>
)}

<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="name">Name</label>
<input type="text" id="name" name="name" required />
</div>

<div className="form-group">
<label htmlFor="email">Email</label>
<input type="email" id="email" name="email" required />
</div>

<div className="form-group">
<label htmlFor="message">Message</label>
<textarea id="message" name="message" rows="4" required></textarea>
</div>

<button
type="submit"
disabled={isPending}
className="submit-button"
>
{isPending ? 'Sending...' : 'Send Message'}
</button>
</form>
</div>
);
}

With Server Actions, the form data is sent directly to the server function without needing to set up a separate API route, which simplifies the process considerably.

Using Form Libraries

For more complex forms, you might want to use a form library like React Hook Form or Formik. These libraries provide additional features like validation, error handling, and form state management.

Example: Using React Hook Form

First, install the library:

bash
npm install react-hook-form

Then implement your form:

jsx
'use client';
import { useForm } from 'react-hook-form';
import { useState } from 'react';

export default function RegistrationForm() {
const {
register,
handleSubmit,
formState: { errors },
reset
} = useForm();

const [isSubmitting, setIsSubmitting] = useState(false);
const [submitResult, setSubmitResult] = useState(null);

const onSubmit = async (data) => {
setIsSubmitting(true);

try {
const response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});

const result = await response.json();

if (!response.ok) {
throw new Error(result.message || 'Registration failed');
}

setSubmitResult({
status: 'success',
message: 'Registration successful!'
});

// Reset the form after successful submission
reset();

} catch (error) {
setSubmitResult({
status: 'error',
message: error.message
});
} finally {
setIsSubmitting(false);
}
};

return (
<div className="registration-form">
<h2>Create an Account</h2>

{submitResult && (
<div className={`alert alert-${submitResult.status}`}>
{submitResult.message}
</div>
)}

<form onSubmit={handleSubmit(onSubmit)}>
<div className="form-group">
<label htmlFor="username">Username</label>
<input
id="username"
{...register('username', {
required: 'Username is required',
minLength: {
value: 3,
message: 'Username must be at least 3 characters'
}
})}
/>
{errors.username && (
<span className="error-message">{errors.username.message}</span>
)}
</div>

<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address'
}
})}
/>
{errors.email && (
<span className="error-message">{errors.email.message}</span>
)}
</div>

<div className="form-group">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
{...register('password', {
required: 'Password is required',
minLength: {
value: 8,
message: 'Password must be at least 8 characters'
}
})}
/>
{errors.password && (
<span className="error-message">{errors.password.message}</span>
)}
</div>

<button
type="submit"
disabled={isSubmitting}
className="submit-button"
>
{isSubmitting ? 'Registering...' : 'Register'}
</button>
</form>
</div>
);
}

React Hook Form provides a more streamlined way to handle form validation and submission, with built-in error handling.

Best Practices for Form Submission in Next.js

  1. Validate on both client and server: Always validate form data on both the client (for immediate feedback) and server (for security).

  2. Show loading states: Indicate when a form is submitting to provide visual feedback to users.

  3. Handle errors gracefully: Display user-friendly error messages when form submission fails.

  4. Use appropriate HTTP status codes: Return appropriate status codes from your API routes (e.g., 200 for success, 400 for bad requests).

  5. Protect against CSRF attacks: Next.js provides built-in CSRF protection when using its form handling.

  6. Consider accessibility: Ensure your forms are accessible by using proper labels, providing feedback for screen readers, and ensuring keyboard navigation works.

  7. Implement rate limiting: For public-facing forms, consider implementing rate limiting to prevent abuse.

Real-World Example: Newsletter Subscription Form

Here's a complete example of a newsletter subscription form with validation, submission handling, and error management:

jsx
'use client';
import { useState } from 'react';

export default function NewsletterForm() {
const [email, setEmail] = useState('');
const [status, setStatus] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);

const validateEmail = (email) => {
return String(email)
.toLowerCase()
.match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
);
};

const handleSubmit = async (e) => {
e.preventDefault();

// Client-side validation
if (!validateEmail(email)) {
setStatus({
type: 'error',
message: 'Please enter a valid email address.'
});
return;
}

setIsSubmitting(true);
setStatus(null);

try {
const response = await fetch('/api/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});

const data = await response.json();

if (!response.ok) {
throw new Error(data.message || 'Failed to subscribe');
}

setStatus({
type: 'success',
message: 'Thank you for subscribing to our newsletter!'
});

// Clear the form
setEmail('');

} catch (error) {
setStatus({
type: 'error',
message: error.message
});
} finally {
setIsSubmitting(false);
}
};

return (
<div className="newsletter-container">
<h2>Subscribe to Our Newsletter</h2>
<p>Stay updated with our latest news and updates.</p>

{status && (
<div className={`alert alert-${status.type}`}>
{status.message}
</div>
)}

<form onSubmit={handleSubmit} className="newsletter-form">
<div className="form-input-group">
<input
type="email"
placeholder="Your email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
aria-label="Email address for newsletter"
className="email-input"
required
/>
<button
type="submit"
disabled={isSubmitting}
className="subscribe-button"
>
{isSubmitting ? 'Subscribing...' : 'Subscribe'}
</button>
</div>
</form>
<p className="privacy-notice">
We respect your privacy. Unsubscribe at any time.
</p>
</div>
);
}

And here's the corresponding API route:

jsx
// app/api/subscribe/route.js
import { NextResponse } from 'next/server';

export async function POST(request) {
try {
const { email } = await request.json();

// Validate email
if (!email || !email.includes('@')) {
return NextResponse.json(
{ message: 'Valid email is required' },
{ status: 400 }
);
}

// Check if already subscribed (example logic)
// const existingSubscriber = await checkIfEmailExists(email);
// if (existingSubscriber) {
// return NextResponse.json(
// { message: 'This email is already subscribed' },
// { status: 400 }
// );
// }

// Add to subscription list (integration with your email service)
// Example:
// await addSubscriber({
// email,
// subscribedAt: new Date(),
// source: 'website'
// });

// For demonstration purposes, just log it
console.log(`New subscription: ${email}`);

return NextResponse.json({
message: 'Successfully subscribed to newsletter'
});

} catch (error) {
console.error('Newsletter subscription error:', error);
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
);
}
}

Summary

In this guide, we've covered various approaches to handle form submissions in Next.js applications:

  1. Client-side form handling with React state and fetch API for basic form submissions
  2. Server-side form processing with Next.js API routes
  3. Server Actions for a more streamlined approach in newer Next.js versions
  4. Form libraries like React Hook Form for more complex forms with validation

We've also explored best practices and provided real-world examples that demonstrate how to implement forms with proper validation, error handling, and user feedback.

By understanding these different approaches, you can choose the most appropriate method for your specific use case, whether it's a simple contact form or a complex multi-step registration process.

Additional Resources

Exercises

  1. Basic Contact Form: Create a simple contact form using React state and Next.js API routes.

  2. Form with File Upload: Extend a basic form to include file upload functionality.

  3. Multi-step Form: Build a multi-step form (like a wizard) with state management between steps.

  4. Dynamic Form Fields: Create a form where users can dynamically add or remove form fields.

  5. Form with Advanced Validation: Implement a form with complex validation rules (e.g., password strength, matching fields).



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)