Skip to main content

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:

  1. Create a file at pages/api/hello.js with the following code:
javascript
export default function handler(req, res) {
res.status(200).json({ message: 'Hello, world!' });
}
  1. Run your Next.js application:
bash
npm run dev
  1. Visit http://localhost:3000/api/hello in your browser, and you should see:
json
{ "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:

Some commonly used methods on the req object:

  • req.method: HTTP method of the request (GET, POST, etc.)
  • req.query: Object containing query parameters
  • req.cookies: Object containing cookies
  • req.body: Object containing body parsed by content type

Some commonly used methods on the res object:

  • res.status(code): Sets the HTTP status code
  • res.json(body): Sends a JSON response
  • res.send(body): Sends a response
  • res.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:

javascript
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:

javascript
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:

json
{ "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:

javascript
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:

javascript
// 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:

javascript
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:

json
{ "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:

javascript
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:

json
{ "message": "Post path data", "path": ["2023", "01", "hello-world"] }

Real-World Examples

1. Contact Form Submission Handler

javascript
// 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

javascript
// 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

javascript
// 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

  1. Use environment variables for secrets

    javascript
    // Bad
    const apiKey = '123456abcdef';

    // Good
    const apiKey = process.env.API_KEY;
  2. Validate input data

    javascript
    export 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...
    }
  3. Implement error handling

    javascript
    export 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' });
    }
    }
  4. 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);
  5. 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:

  1. Cold starts: Serverless functions may experience latency when they haven't been used recently.

  2. Execution time limits: Most serverless platforms impose time limits (e.g., 10 seconds on Vercel).

  3. Statelessness: Serverless functions don't maintain state between invocations.

  4. 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

Exercises

  1. Create a simple todo API with endpoints to list, create, update, and delete todo items.
  2. Implement an API route that fetches data from an external API and transforms it before sending it to the client.
  3. Build a simple user authentication system using JWT tokens and API routes.
  4. Create a file upload handler that saves files to a service like AWS S3.
  5. 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! :)