Next.js Lesson 10 – API Routes | Dataplexa
Lesson 10

API Routes

Build server endpoints that handle newsletter signups, article queries, and user data for NewsWave

Traditional React applications run entirely in the browser. The component renders, fetches data from external APIs, and updates the interface based on user interactions. But what happens when you need to process payments, send emails, or store sensitive data? You need a server. Next.js solves this with API Routes — server-side endpoints that run on the same domain as your application. Think of them as mini Express.js routes built directly into your Next.js project. Unlike client-side code that everyone can inspect in the browser, API routes execute on the server where your secrets stay secret. The NewsWave team wants users to subscribe to their newsletter. Building a traditional subscription flow requires setting up a separate backend server, configuring CORS, and managing two different deployments. API Routes eliminate this complexity by letting you write server logic alongside your pages.

Creating Your First API Route

API Routes live in a special pages/api directory. Any file you create here becomes an HTTP endpoint. When someone visits /api/hello, Next.js executes your server code and returns the response. The magic happens through file-based routing, just like pages. Create pages/api/hello.js and Next.js automatically creates the /api/hello endpoint. This approach feels natural because you already understand how page routing works. Every API route exports a default function that receives two parameters: req (request) and res (response). The request contains data from the client — form submissions, query parameters, headers. The response lets you send data back — JSON, status codes, cookies.
// pages/api/hello.js — NewsWave's first API endpoint
export default function handler(req, res) {
  // req = incoming request data from the browser
  // res = outgoing response we send back
  
  res.status(200).json({ 
    message: 'Welcome to NewsWave API',
    timestamp: new Date().toISOString()
  })
}
Terminal
$ curl http://localhost:3000/api/hello
{"message":"Welcome to NewsWave API","timestamp":"2024-01-15T10:30:00.000Z"}
✓ API route responding successfully

What just happened?

Next.js automatically created a server endpoint at /api/hello. When you visit this URL, the handler function runs on the server (not in the browser) and returns JSON data. The status(200) indicates success, while json() sets the content type and sends the response.

Building a Newsletter Subscription Endpoint

Real applications need to handle different HTTP methods. A newsletter signup should accept POST requests containing email addresses, validate the data, and store the subscription. API routes handle this by checking the request method and branching logic accordingly. The request object contains everything the client sends. Form data arrives in req.body, URL parameters in req.query, and HTTP headers in req.headers. The response object provides methods to send different types of responses back to the client. Think of API routes like a restaurant kitchen. The waiter (browser) brings orders (requests) to the kitchen (API route). The chef (your handler function) processes the order based on what was requested and sends back the finished dish (response). Different orders require different preparation methods.
// pages/api/newsletter.js — Handle NewsWave newsletter signups
export default function handler(req, res) {
  // Only accept POST requests for newsletter signup
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }
  
  // Extract email from request body
  const { email } = req.body
}
Now add validation to ensure the email address is properly formatted. Server-side validation is crucial because anyone can bypass client-side checks by sending direct HTTP requests to your API.
// pages/api/newsletter.js — Add email validation
export default function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }
  
  const { email } = req.body
  
  // Validate email format using a simple regex
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  
  if (!email || !emailRegex.test(email)) {
    return res.status(400).json({ error: 'Valid email address required' })
  }
Complete the endpoint by simulating the subscription storage and sending a success response. In production, you would save this to a database or send it to an email service like Mailchimp.
// pages/api/newsletter.js — Complete newsletter endpoint
export default function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }
  
  const { email } = req.body
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  
  if (!email || !emailRegex.test(email)) {
    return res.status(400).json({ error: 'Valid email address required' })
  }
  
  // TODO: Save email to database or email service
  console.log(`New newsletter signup: ${email}`)
  
  // Send success response
  res.status(201).json({ 
    success: true,
    message: 'Successfully subscribed to NewsWave newsletter' 
  })
}
Terminal
$ curl -X POST http://localhost:3000/api/newsletter \
-H "Content-Type: application/json" \
-d '{"email":"reader@example.com"}'
{"success":true,"message":"Successfully subscribed to NewsWave newsletter"}
✓ Newsletter signup completed

What just happened?

The API route checked the HTTP method, validated the email format, and returned appropriate responses. Status code 405 means "method not allowed", 400 means "bad request", and 201 means "created successfully". This pattern — validate input, process data, return response — forms the foundation of most API endpoints.

Connecting Frontend Forms to API Routes

API routes become powerful when connected to your React components. NewsWave needs a newsletter signup form that posts data to your API endpoint. This creates a seamless full-stack experience where the frontend and backend share the same domain and deployment. Building this connection requires understanding how React's state management integrates with HTTP requests. The form captures user input in local state, validates it client-side for immediate feedback, then sends it to your API route for server-side processing. Unlike external APIs that might have CORS restrictions or authentication complexities, your API routes are part of the same application. This means simpler requests, shared environment variables, and unified error handling.
// components/NewsletterForm.js — NewsWave newsletter signup form
import { useState } from 'react'

export default function NewsletterForm() {
  // Track form state and submission status
  const [email, setEmail] = useState('')
  const [status, setStatus] = useState('idle') // idle, submitting, success, error
  const [message, setMessage] = useState('')
}
Add the form submission handler that sends data to your API route. The fetch function sends a POST request to /api/newsletter with the email data.
// components/NewsletterForm.js — Add form submission handler
const handleSubmit = async (e) => {
  e.preventDefault() // Prevent page reload
  setStatus('submitting')
  
  try {
    // Send POST request to our API route
    const response = await fetch('/api/newsletter', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email })
    })
    
    const data = await response.json()
    
    if (response.ok) {
      setStatus('success')
      setMessage(data.message)
      setEmail('') // Clear the form
    } else {
      setStatus('error')
      setMessage(data.error)
    }
  } catch (error) {
    setStatus('error')
    setMessage('Network error. Please try again.')
  }
}
Complete the component with the JSX form that renders different states based on the submission status.
// components/NewsletterForm.js — Complete form component
return (
  <div style={{ maxWidth: '400px', margin: '0 auto', padding: '20px' }}>
    <h3>Subscribe to NewsWave</h3>
    
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Enter your email"
        disabled={status === 'submitting'}
        style={{ width: '100%', padding: '12px', margin: '10px 0' }}
      />
      
      <button 
        type="submit" 
        disabled={status === 'submitting'}
        style={{ 
          width: '100%', padding: '12px', 
          background: status === 'submitting' ? '#ccc' : '#0369a1',
          color: 'white', border: 'none', borderRadius: '4px'
        }}
      >
        {status === 'submitting' ? 'Subscribing...' : 'Subscribe'}
      </button>
    </form>
    
    {message && (
      <p style={{ 
        color: status === 'success' ? '#15803d' : '#dc2626',
        marginTop: '10px' 
      }}>
        {message}
      </p>
    )}
  </div>
)
localhost:3000 — NewsWave

What just happened?

The React form communicates with your API route through a POST request. The component manages loading states, shows success messages, and handles errors gracefully. This creates a smooth user experience where the form feels responsive and provides clear feedback. Try entering an invalid email to see the validation in action.

Dynamic API Routes and Query Parameters

Just like pages, API routes support dynamic segments using square brackets. NewsWave needs an endpoint to fetch individual articles by their slug — /api/articles/breaking-news-update should return that specific article's data. Dynamic API routes follow the same naming convention as dynamic pages. Create pages/api/articles/[slug].js and Next.js makes the slug available in req.query.slug. This pattern scales to complex hierarchies like /api/users/[id]/posts/[postId]. Query parameters also appear in req.query. A request to /api/articles/tech-news?limit=5&sort=date gives you { slug: 'tech-news', limit: '5', sort: 'date' } in the query object.
// pages/api/articles/[slug].js — Fetch NewsWave articles by slug
export default function handler(req, res) {
  const { slug } = req.query // Extract slug from URL
  
  // Mock article database
  const articles = {
    'breaking-tech-news': {
      id: 1,
      title: 'Revolutionary AI Breakthrough Announced',
      author: 'Sarah Chen',
      category: 'Tech',
      views: 15420
    },
    'market-update-today': {
      id: 2,
      title: 'Stock Markets Rally on Economic Data',
      author: 'Michael Rodriguez',
      category: 'Business', 
      views: 8932
    }
  }
Add logic to find the requested article and handle cases where it doesn't exist. Real applications would query a database, but this mock data demonstrates the pattern.
// pages/api/articles/[slug].js — Complete article lookup
export default function handler(req, res) {
  const { slug } = req.query
  
  const articles = {
    'breaking-tech-news': {
      id: 1,
      title: 'Revolutionary AI Breakthrough Announced',
      author: 'Sarah Chen',
      category: 'Tech',
      views: 15420
    },
    'market-update-today': {
      id: 2, 
      title: 'Stock Markets Rally on Economic Data',
      author: 'Michael Rodriguez',
      category: 'Business',
      views: 8932
    }
  }
  
  const article = articles[slug]
  
  if (!article) {
    return res.status(404).json({ error: 'Article not found' })
  }
  
  // Return the article data
  res.status(200).json(article)
}
Terminal
$ curl http://localhost:3000/api/articles/breaking-tech-news
{"id":1,"title":"Revolutionary AI Breakthrough Announced","author":"Sarah Chen","category":"Tech","views":15420}
$ curl http://localhost:3000/api/articles/nonexistent
{"error":"Article not found"}

What just happened?

The dynamic route captured the slug from the URL and used it to lookup article data. When the article exists, it returns the data with a 200 status. When it doesn't exist, it returns a 404 error. This pattern works for any dynamic data lookup — users, products, categories.

Error Handling and Status Codes

Professional APIs communicate clearly through HTTP status codes and consistent error formats. Status codes tell clients exactly what happened — success, client error, server error. Consistent error responses help frontend developers build reliable error handling. The 2xx range indicates success: 200 for general success, 201 for resource creation, 204 for successful deletion with no content. The 4xx range indicates client errors: 400 for bad requests, 401 for authentication required, 404 for not found, 405 for wrong HTTP method. The 5xx range indicates server errors: 500 for internal errors, 503 for service unavailable. Error responses should include helpful messages that explain what went wrong and potentially how to fix it. But be careful not to expose sensitive information like database errors or system paths in production error messages.
// pages/api/contact.js — NewsWave contact form with proper error handling
export default function handler(req, res) {
  try {
    // Only accept POST requests
    if (req.method !== 'POST') {
      return res.status(405).json({ 
        error: 'Method not allowed',
        message: 'This endpoint only accepts POST requests' 
      })
    }
    
    const { name, email, message } = req.body
Add comprehensive validation with specific error messages for each missing or invalid field.
    // Validate required fields
    if (!name || name.trim().length < 2) {
      return res.status(400).json({
        error: 'Validation failed',
        message: 'Name must be at least 2 characters long'
      })
    }
    
    if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      return res.status(400).json({
        error: 'Validation failed', 
        message: 'Please provide a valid email address'
      })
    }
    
    if (!message || message.trim().length < 10) {
      return res.status(400).json({
        error: 'Validation failed',
        message: 'Message must be at least 10 characters long'
      })
    }
Complete the handler with success response and catch any unexpected errors.
    // Process the contact form (would save to database/send email)
    console.log('New contact form submission:', { name, email, message })
    
    // Return success response
    res.status(201).json({
      success: true,
      message: 'Thank you for contacting NewsWave. We will respond within 24 hours.'
    })
    
  } catch (error) {
    // Handle unexpected server errors
    console.error('Contact form error:', error)
    res.status(500).json({
      error: 'Internal server error',
      message: 'Something went wrong. Please try again later.'
    })
  }
}
Terminal
$ curl -X POST http://localhost:3000/api/contact \
-H "Content-Type: application/json" \
-d '{"name":"","email":"invalid","message":"Hi"}'
{"error":"Validation failed","message":"Name must be at least 2 characters long"}
✓ Proper validation error returned

What just happened?

The API route validates each field separately and returns specific error messages. The try-catch block handles unexpected errors gracefully. Status codes tell the client exactly what type of error occurred, while the error messages provide actionable feedback for fixing the request.

Understanding API Routes vs External APIs

API Routes differ fundamentally from external APIs in deployment, security, and data flow. Understanding these differences helps you decide when to build your own endpoints versus integrating with third-party services. External APIs run on different servers, require network requests across the internet, and often need API keys or authentication tokens. They might have rate limits, downtime, or versioning changes that affect your application. But they provide specialized functionality you don't want to build yourself — payment processing, email delivery, social media integration. API Routes run on the same server as your Next.js application. They share environment variables, database connections, and deployment pipelines. This tight integration enables powerful patterns like server-side authentication, data aggregation from multiple sources, or custom business logic that external APIs can't provide.

API Routes (Internal)

  • Same domain and deployment
  • Share environment variables
  • No CORS restrictions
  • Custom business logic
  • Database access

External APIs

  • Third-party services
  • API keys and authentication
  • Rate limits and quotas
  • Specialized functionality
  • Network latency
NewsWave might use API Routes for newsletter signups, user preferences, and article searches while integrating external APIs for payment processing (Stripe), email delivery (SendGrid), and social sharing (Twitter API). This hybrid approach maximizes both control and functionality. The key decision point: build API Routes for data and logic specific to your application, integrate external APIs for specialized services that would take months to build yourself.

Production Considerations and Best Practices

Production API routes require additional considerations around security, performance, and monitoring. Unlike development where you control all requests, production APIs face real users, potential abuse, and scaling challenges. Rate limiting prevents abuse by restricting how many requests a single IP address can make within a time window. Without rate limiting, a single user could overwhelm your server with thousands of newsletter signup attempts. Most production applications implement rate limiting at the infrastructure level, but you can add basic protection directly in your API routes. Input sanitization protects against injection attacks where malicious users send harmful data in request bodies. Always validate and sanitize user input before processing it, especially before database queries or email sending.

Security Checklist

Validate all inputs: Check types, formats, and ranges before processing

Use HTTPS: Encrypt all data in transit with SSL certificates

Sanitize outputs: Escape user content before displaying in responses

Environment variables: Store secrets in env files, never in code

Error logging helps debug issues in production where you can't see console output. Services like Sentry or LogRocket capture API errors with full request context, making it easier to identify and fix problems. Performance monitoring tracks response times, error rates, and throughput. Slow API routes affect user experience just like slow pages. Consider caching frequently requested data and optimizing database queries for popular endpoints.

Quiz

1. How does NewsWave create the endpoint /api/newsletter in Next.js?


2. In the API route pages/api/articles/[slug].js, how do you access the article slug from the URL?


3. What should NewsWave's newsletter API return when someone submits an invalid email format?


Up Next: Data Fetching Basics

Learn how to fetch data from your API routes and external services using getStaticProps and getServerSideProps for optimal performance.