devcrea.com
Web & Frontend

Netlify Serverless Functions: A Beginner's Guide to the Modern API

Build Netlify serverless functions with the modern Request/Response API. Setup, custom routes, API key proxying, CORS fixes, and the production 404 fix.

Netlify Serverless Functions: A Beginner's Guide to the Modern API

You spent hours setting up a Netlify function. It runs perfectly on your machine with netlify dev, but after you deploy it, the live URL returns a 404 with no useful explanation. Many tutorials still use the legacy API, so their event handler examples can lead you astray when you need modern routing.

  • V2 syntax: Uses standard web Request and Response objects
  • Routing: Defined in an exported config object
  • Default directory: netlify/functions/ (configurable in netlify.toml)
  • Local dev: Run netlify dev to test with Netlify's local development environment
  • Sync timeout: 10 seconds on the free tier

Legacy vs. Modern API: What Changed

For years, Netlify functions used AWS Lambda-style syntax. You exported a handler that received an event and returned an object with a statusCode and a stringified body. It worked, but it was different from the APIs used elsewhere on the web.

The modern API uses the standard Request and Response objects. Your serverless function can now look much like code written for Edge functions or modern full-stack frameworks.

There's no need to parse a JSON string from an event body or remember a Netlify-specific response format. Just return a standard Response.json().

Setting Up Your First Modern Netlify Function

With the modern API, you can give a function a clean, custom URL directly in its file instead of relying on the old /.netlify/functions/your-function URL structure.

Prerequisites and Local Environment

Start by creating a netlify/functions directory at the root of your project. If you use another folder, specify it in netlify.toml under the [build] section.

If you haven't installed the Netlify CLI, run npm install -g netlify-cli, then authenticate with ntl login. The CLI runs on Node, so check that you're using a current Node.js version before you begin. Older releases may fail to bundle functions that use the newer syntax.

Advertisement

During development, run your site with netlify dev. It starts a local server that applies Netlify's routing, environment variables, and proxy rules.

Using the Web Request and Response Objects

Create hello.mts (or .ts / .js) in your functions directory. Here's the syntax for a basic endpoint:

import type { Context } from "@netlify/functions"

export default async (req: Request, context: Context) => {
  const url = new URL(req.url)
  const name = url.searchParams.get("name") || "World"

  return new Response(`Hello, ${name}!`, {
    status: 200,
    headers: { "Content-Type": "text/plain" }
  })
}

The function reads query parameters through the standard URL interface and returns a native Response.

Defining Custom Routes with the Config Export

With the legacy API, mapping a function to a URL such as /api/hello meant configuring a _redirects file. The V2 API lets you define the route in the function file itself.

Export a config object with the path:

import type { Config } from "@netlify/functions"

export default async (req: Request) => {
  return Response.json({ message: "System online" })
}

export const config: Config = {
  path: "/api/status"
}

After deployment, Netlify routes requests to /api/status to your function.

Real-World Serverless Use Cases

A function can do much more than return "Hello World." It exposes a public endpoint, so the same REST API design best practices that guide other backends apply here. Here's how the modern syntax handles common production tasks.

Advertisement

Hiding API Keys via Proxy

If you call a paid service, such as ElevenLabs for text-to-speech or a translation API, keep its API key out of your frontend code. A serverless function can handle the request on behalf of your frontend.

Your frontend, regardless of the React build tool or framework you use, sends a request to the Netlify function. The function reads the required environment variables and forwards the request to the external API.

import type { Config } from "@netlify/functions"

export default async (req: Request) => {
  // The frontend only sees this function, not your actual API key
  const requestData = await req.json()

  const response = await fetch("https://api.elevenlabs.io/v1/text-to-speech", {
    method: "POST",
    headers: {
      "xi-api-key": process.env.ELEVENLABS_SECRET_KEY!,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(requestData)
  })

  const audioBuffer = await response.arrayBuffer()
  return new Response(audioBuffer, {
    headers: { "Content-Type": "audio/mpeg" }
  })
}

export const config: Config = { path: "/api/synthesize" }

Handling Webhooks

E-commerce stores and SaaS products often need to process payment events or deliver digital products as soon as a webhook arrives. A serverless function can receive the webhook, validate its payload, and trigger other services without a dedicated Express server.

Set the webhook URL in the sending platform's dashboard to your custom function path. The function runs when an event arrives and processes the data, with no server for you to maintain. Before acting on an event, read the raw request body and verify the sender's signature. Most providers send an HMAC signature header generated with a shared secret; checking it helps prevent someone from forging a payment event through your public endpoint.

Troubleshooting: Why Does It 404 in Production?

Your site works with netlify dev, but the live URL says Not Found. Work through these checks to find out why.

The Functions Directory Isn't Configured

If your code isn't in the default netlify/functions folder, tell Netlify where to find it. For example, a custom directory such as src/api needs to be configured explicitly.

Create or update netlify.toml at the root of your repository:

Advertisement
[build]
  functions = "src/api"

Check that the functions directory is committed to Git, too. It may have been included in .gitignore by mistake.

Missing CORS and OPTIONS Preflight Headers

If your frontend and Netlify backend are on different domains, the browser may block requests under its CORS (Cross-Origin Resource Sharing) policy. For some POST requests, the browser first sends an OPTIONS preflight request to check what's allowed.

If your function doesn't handle that OPTIONS request, the browser stops before sending the POST. The resulting network error can be mistaken for a 404.

export default async (req: Request) => {
  const headers = {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "Content-Type",
    "Access-Control-Allow-Methods": "GET, POST, OPTIONS"
  }

  // Intercept the preflight request immediately
  if (req.method === "OPTIONS") {
    return new Response(null, { status: 204, headers })
  }

  return Response.json({ success: true }, { headers })
}

Environment Variable Deployment Quirks

A missing production environment variable can also cause a function to fail. process.env.MY_KEY may work locally because it reads your .env file, while production doesn't read committed .env files for security reasons.

Add the variables in the Netlify dashboard under Site Settings, then Environment Variables.

After changing a variable in the dashboard, trigger a new deployment by pushing a commit or selecting "Trigger deploy." The change won't necessarily reach the live function until you redeploy.

The 404 Diagnosis Table

If the live URL fails, check these signs in order. They cover common reasons a function works locally but fails in production.

Advertisement
Symptom Likely cause One-line fix
Deploy log shows 0 functions packaged Wrong functions directory Set [build] functions = "your/dir" in netlify.toml
Browser shows a CORS or network error on POST No OPTIONS preflight handler Return 204 with Access-Control-* headers
Function runs but a value is undefined Env var set but not deployed Add it in the dashboard, then redeploy

Deploying to Netlify Production

Once local testing is complete and your environment variables are set in the dashboard, push your code to your connected Git repository on GitHub, GitLab, or Bitbucket.

Netlify detects the functions directory, bundles dependencies, reads the config path exports, and deploys the endpoints. Check the deploy log for the number of functions packaged and deployed.

Keep the execution limit in mind once your endpoints are live. Synchronous functions have 10 seconds on the free tier. For longer work, such as converting a large file or waiting on a slow external service, use a background function by adding a -background suffix to the filename so the job can continue beyond that limit.

Frequently asked

Questions & answers

Where should I put Netlify functions in my project?

Put them in a netlify/functions folder at the root of your repository. If you use another folder, set the functions key under the build section in netlify.toml to point to it. Netlify only bundles functions in the configured directory.

Why do my Netlify functions work locally but return 404 in production?

The live site may not be able to find your functions because the directory is configured incorrectly or wasn't committed to Git. Check the deploy log for the number of functions packaged. If it says zero, check the path in netlify.toml and the entry in .gitignore.

How do I use environment variables in a Netlify function?

Read them inside the function with process.env.YOUR_KEY. Set their values in the Netlify dashboard under Site Settings, then Environment Variables, rather than committing a .env file. Redeploy after adding a variable so the live function can use it.

How long can a Netlify function run before it times out?

Synchronous functions time out after 10 seconds on the free tier. For longer jobs, add a -background suffix to the filename to run the work as a background function beyond that limit.

How do I fix CORS errors in Netlify functions?

Return the Access-Control-Allow-Origin, Allow-Headers, and Allow-Methods headers from your function. Respond to the browser's OPTIONS preflight with a 204 status before handling the actual request. Without a preflight response, a CORS error can look like a 404.

Should I use the event/context handler or the modern Request/Response API?

Use the modern Request/Response API for new functions. The old event/context handler still works, but it's the legacy approach. The newer API uses standard web objects, supports custom routes through a config export, and follows the pattern used by Edge functions and modern frameworks.

Share Article