Next.js Serverless Functions
Introduction
Serverless functions represent a modern approach to backend development that allows developers to write and deploy individual functions without managing server infrastructure. Next.js, a popular React framework, includes built-in support for serverless functions through its API Routes feature, making it easy to create serverless endpoints directly within your application.
In this guide, you'll learn:
- What serverless functions are and their benefits
- How to create API routes in Next.js
- How to handle different HTTP methods
- How to access query parameters and request body data
- Best practices for organizing and implementing serverless functions
- Real-world applications and use cases
What are Serverless Functions?
Serverless functions (sometimes called Function-as-a-Service or FaaS) are individual pieces of code that run on-demand in response to specific events, such as HTTP requests. Unlike traditional server architectures, serverless functions:
- Scale automatically based on demand
- Only run when needed (and you only pay for what you use)
- Don't require server maintenance or configuration
- Can be deployed independently
In Next.js, serverless functions take the form of API routes, which are special files located in the pages/api
directory that automatically become API endpoints.
Creating Your First API Route
Let's create a simple API route that returns a greeting message:
- Create a file at
pages/api/hello.js
with the following code:
export default function handler(req, res) {
res.status(200).json({ message: 'Hello, world!' });
}
- Run your Next.js application:
npm run dev
- Visit
http://localhost:3000/api/hello
in your browser, and you should see:
{ "message": "Hello, world!" }
Congratulations! You've just created your first serverless function in Next.js.
Understanding the API Route Handler
The API route handler function receives two parameters:
req
: An instance of http.IncomingMessage, plus some pre-built middlewaresres
: An instance of http.ServerResponse, plus some helper functions
Some commonly used methods on the req
object:
req.method
: HTTP method of the request (GET, POST, etc.)req.query
: Object containing query parametersreq.cookies
: Object containing cookiesreq.body
: Object containing body parsed by content type
Some commonly used methods on the res
object:
res.status(code)
: Sets the HTTP status coderes.json(body)
: Sends a JSON responseres.send(body)
: Sends a responseres.redirect([status,] path)
: Redirects to a specified path
Handling Different HTTP Methods
API routes can handle different HTTP methods (GET, POST, PUT, DELETE, etc.) within the same function:
export default function handler(req, res) {
switch (req.method) {
case 'GET':
return res.status(200).json({ message: 'This is a GET request' });
case 'POST':
return res.status(200).json({ message: 'This is a POST request' });
case 'PUT':
return res.status(200).json({ message: 'This is a PUT request' });
case 'DELETE':
return res.status(200).json({ message: 'This is a DELETE request' });
default:
return res.status(405).json({ message: 'Method not allowed' });
}
}
Working with Query Parameters
Query parameters can be accessed through req.query
:
Create a file at pages/api/greet.js
:
export default function handler(req, res) {
const { name = 'Guest' } = req.query;
res.status(200).json({ message: `Hello, ${name}!` });
}
Now, you can visit http://localhost:3000/api/greet?name=John
and you'll see:
{ "message": "Hello, John!" }
Handling POST Requests with Request Body
For POST requests, you can access the request body through req.body
:
Create a file at pages/api/echo.js
:
export default function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Only POST requests allowed' });
}
const data = req.body;
res.status(200).json({
message: 'Data received',
data: data
});
}
You can test this API route using tools like Postman, curl, or a simple fetch request:
// Client-side code
const response = await fetch('/api/echo', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ hello: 'world' }),
});
const data = await response.json();
console.log(data);
// Output: { message: 'Data received', data: { hello: 'world' } }
Dynamic API Routes
Just like regular Next.js pages, API routes can be dynamic:
Create a file at pages/api/users/[id].js
:
export default function handler(req, res) {
const { id } = req.query;
res.status(200).json({
message: `User information for ID: ${id}`,
userId: id
});
}
Now, you can access user information at http://localhost:3000/api/users/123
and get:
{ "message": "User information for ID: 123", "userId": "123" }
Catch-All API Routes
You can also create catch-all API routes for handling more complex paths:
Create a file at pages/api/posts/[...slug].js
:
export default function handler(req, res) {
const { slug } = req.query;
res.status(200).json({
message: 'Post path data',
path: slug
});
}
Accessing http://localhost:3000/api/posts/2023/01/hello-world
will return:
{ "message": "Post path data", "path": ["2023", "01", "hello-world"] }
Real-World Examples
1. Contact Form Submission Handler
// pages/api/contact.js
import nodemailer from 'nodemailer';
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}
const { name, email, message } = req.body;
// Validate inputs
if (!name || !email || !message) {
return res.status(400).json({ message: 'Missing required fields' });
}
try {
// Setup nodemailer (in a real app, use environment variables for credentials)
const transporter = nodemailer.createTransport({
host: 'smtp.example.com',
port: 587,
secure: false,
auth: {
user: '[email protected]',
pass: 'your-password',
},
});
// Send email
await transporter.sendMail({
from: '[email protected]',
to: '[email protected]',
subject: `New Contact Form Submission from ${name}`,
text: `Name: ${name}\nEmail: ${email}\nMessage: ${message}`,
});
return res.status(200).json({ message: 'Message sent successfully' });
} catch (error) {
console.error('Error sending email:', error);
return res.status(500).json({ message: 'Error sending message' });
}
}
2. Simple Authentication API
// pages/api/login.js
import { sign } from 'jsonwebtoken';
import { compare } from 'bcrypt';
import cookie from 'cookie';
// Mock user database (in a real app, use a proper database)
const users = [
{
id: 1,
email: '[email protected]',
// hashed password for 'password123'
passwordHash: '$2b$10$zH.Wj7OTV5VsN.5xbU8ScuF7xov/dZJYTQPQtGtS1.qI/ApqmEEHe',
},
];
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}
const { email, password } = req.body;
// Find user
const user = users.find(u => u.email === email);
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Verify password
const passwordMatches = await compare(password, user.passwordHash);
if (!passwordMatches) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Generate JWT token (use a secure SECRET in production)
const token = sign({ userId: user.id, email: user.email }, 'SECRET', {
expiresIn: '1h',
});
// Set cookie
res.setHeader('Set-Cookie', cookie.serialize('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV !== 'development',
maxAge: 60 * 60, // 1 hour
sameSite: 'strict',
path: '/',
}));
return res.status(200).json({ message: 'Login successful', user: { id: user.id, email: user.email } });
}
3. External API Proxy
// pages/api/weather.js
export default async function handler(req, res) {
const { city } = req.query;
if (!city) {
return res.status(400).json({ message: 'City parameter is required' });
}
try {
// Replace this with your actual API key
const API_KEY = process.env.WEATHER_API_KEY;
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&units=metric&appid=${API_KEY}`
);
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Failed to fetch weather data');
}
return res.status(200).json({
city: data.name,
temperature: data.main.temp,
description: data.weather[0].description,
icon: data.weather[0].icon,
});
} catch (error) {
console.error('Weather API error:', error);
return res.status(500).json({ message: 'Error fetching weather data', error: error.message });
}
}
Best Practices for Next.js API Routes
-
Use environment variables for secrets
javascript// Bad
const apiKey = '123456abcdef';
// Good
const apiKey = process.env.API_KEY; -
Validate input data
javascriptexport default function handler(req, res) {
const { email } = req.body;
if (!email || !email.includes('@')) {
return res.status(400).json({ message: 'Valid email is required' });
}
// Continue processing...
} -
Implement error handling
javascriptexport default async function handler(req, res) {
try {
// Your code here
} catch (error) {
console.error('API error:', error);
return res.status(500).json({ message: 'Internal server error' });
}
} -
Use middleware for common operations
javascript// middleware/withAuth.js
export function withAuth(handler) {
return async (req, res) => {
// Check for authentication
const token = req.cookies.token;
if (!token) {
return res.status(401).json({ message: 'Unauthorized' });
}
// Verify token logic here...
return handler(req, res);
};
}
// pages/api/protected-route.js
import { withAuth } from '../../middleware/withAuth';
function handler(req, res) {
return res.status(200).json({ message: 'Protected data' });
}
export default withAuth(handler); -
Organize routes for complex APIs
/pages/api/users/index.js // GET /api/users - List all users
/pages/api/users/[id].js // GET /api/users/:id - Get a specific user
Limitations and Considerations
While serverless functions are powerful, they come with certain limitations:
-
Cold starts: Serverless functions may experience latency when they haven't been used recently.
-
Execution time limits: Most serverless platforms impose time limits (e.g., 10 seconds on Vercel).
-
Statelessness: Serverless functions don't maintain state between invocations.
-
Limited resources: Memory and CPU constraints vary by provider.
For these reasons, serverless functions are best suited for:
- API endpoints that handle HTTP requests
- Processing webhooks
- Background tasks that fit within timeout limits
- Data transformations and processing
For more complex applications, you might need to combine serverless functions with other backend services.
Summary
Next.js serverless functions provide a powerful and convenient way to add backend capabilities to your application without managing traditional server infrastructure. Through API routes, you can:
- Create API endpoints by simply adding files to the
pages/api
directory - Handle different HTTP methods and access request data
- Implement dynamic and catch-all routes
- Connect to databases and external services
- Build full-stack applications with minimal configuration
Serverless functions are particularly useful for:
- Form submissions
- Authentication
- Data fetching and transformation
- Integration with third-party APIs
- Simple CRUD operations
As you continue developing with Next.js, you'll find that serverless functions provide just the right balance of simplicity and power for many backend tasks.
Additional Resources
- Official Next.js API Routes documentation
- Vercel Serverless Functions documentation
- Handling forms in Next.js
- API Routes with MongoDB
Exercises
- Create a simple todo API with endpoints to list, create, update, and delete todo items.
- Implement an API route that fetches data from an external API and transforms it before sending it to the client.
- Build a simple user authentication system using JWT tokens and API routes.
- Create a file upload handler that saves files to a service like AWS S3.
- Implement rate limiting for your API routes to prevent abuse.
By mastering Next.js serverless functions, you'll be equipped to build dynamic, full-stack applications without the complexity of managing separate backend infrastructure.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)