netlify/context-and-tools/netlify-edge-functions

Claude Code and agent skill by netlify/context-and-tools

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Write, route, cache, and debug Netlify Edge Functions, the Deno-based TypeScript or JavaScript handlers in netlify/edge-functions that run at the network edge in front of static files and serverless functions. Activate this skill whenever someone wants to add auth middleware or a login gate, redirect or rewrite visitors by country or cookie, localize or personalize a page with geolocation, run A/B testing, transform or inject HTML into a response, stream server-side rendering at the edge, or ca…

Install

npx vskill@latest install netlify/context-and-tools/netlify-edge-functions

SKILL.md

Netlify Edge Functions

Use this form

ts
// netlify/edge-functions/hello.ts
import type { Config, Context } from "@netlify/edge-functions";
export default async (request: Request, context: Context) => {
return new Response(`Hello from ${context.geo.country?.name ?? "somewhere"}`);
};
export const config: Config = { path: "/hello" };
  • One file = one function: a default-export handler plus a named config export with the route. Alternatively, declare the route in netlify.toml.
  • The default export can also be a Fetchable module: an object with a fetch method. Netlify calls fetch with the same (request, context) arguments and uses its return value the same way; inside fetch, this is the exported object. Use it to share one handler with other platforms that accept Fetchable modules.

``ts export default { async fetch(request: Request, context: Context) { return new Response("Hello"); }, }; export const config: Config = { path: "/hello" }; // still a named export ``

  • Request and Response are global. Import Context and Config types from @netlify/edge-functions.
  • Read env vars with Netlify.env.get(). process.env works only for Next.js middleware.

Avoid these:

  • A file with no config.path or pattern and no [[edge_functions]] entry. It deploys but never runs.
  • Import maps in deno.json. They are unsupported. Use a separate file with [functions] deno_import_map.
  • Variables set in netlify.toml. They are invisible to edge functions.
  • Netlify.env.set() or Netlify.env.delete() as persistence. They affect only the current invocation.
  • fetch() to the same site for the current path. Use context.next() instead.

⚠️ Footguns

  • No route = silent no-op. Edge functions get no automatic URL.
  • `path: "/*"` runs on every request, including assets. Narrow it, or add excludedPath.
  • Caching needs both parts: cache: "manual" in config AND cache headers on the returned Response. With only one, nothing is cached.
  • Cached functions shadow static files. A cached function on /* serves /cat.png instead of the file.
  • Returning a `Response` ends the chain. Redirect rules for that path never run.
  • Same-site `fetch()` and `URL` rewrites start a new request chain. Matching edge functions run again. Use context.next() to reach the static asset or serverless function at the same path.
  • The request body can be read once. After await req.json(), pass new Request(req, { body }) to context.next().
  • Same-named `foo.ts` and `foo.js`: the TS file is ignored and the JS file deploys.
  • 50 ms CPU per request, including waitUntil work.
  • Not executed on sites with Split Testing enabled.
  • Custom Headers and basic-auth headers do not apply to edge functions.
  • Not supported on HIPAA-compliant hosting.
  • `netlify.app` is on the Public Suffix List. Cross-subdomain cookies need a custom domain.

Files

WhatWhere
Functions<base>/netlify/edge-functions/*.{js,ts,jsx,tsx}
Custom dir[build] edge_functions = "my-dir" (relative to base dir; keep it outside the publish dir)
Name in tomlfile name without extension (auth.ts → function = "auth")
Import mapany JSON file, referenced by [functions] deno_import_map = "./import_map.json"

Return values

ReturnEffect
Responsesent to client; chain ends
URLsame-site rewrite, 200, address bar unchanged
undefined / return;skip this function; chain continues
Endpoint
ts
export default async () =>
new Response("Hello, World!", { headers: { "content-type": "text/html" } });
export const config = { path: "/api/hello" };
Redirect / rewrite
ts
import type { Context } from "@netlify/edge-functions";
export default async (req: Request, { cookies, geo }: Context) => {
if (geo.city === "Paris" && cookies.get("promo-code") === "15-for-followers") {
return Response.redirect(new URL("/subscriber-sale", req.url)); // redirect
// or: return new URL("/subscriber-sale", req.url); // rewrite (same-site only)
}
};
export const config = { path: "/sale" };

Rewrites must target the same site. To reach other sites, use fetch.

Middleware (modify origin response)
ts
import type { Context } from "@netlify/edge-functions";
export default async (request: Request, context: Context) => {
if (new URL(request.url).searchParams.get("method") !== "transform") return;
const response = await context.next();
return new Response((await response.text()).toUpperCase(), response);
};
export const config = { path: "/blog/*" };

Call context.next() only when you need the response body. If you just return undefined, the chain continues on its own.

Conditional requests
ts
export default async (req: Request, { next }: Context) => {
const res = await next({ sendConditionalRequest: true });
if (res.status === 304) return res;
return new Response((await res.text()).toUpperCase(), res);
};

By default, next() strips conditional headers, so it always returns a full response.

Read body, then continue
ts
export default async (req: Request, context: Context) => {
const body = await req.json();
if (!isValid(body.access_token)) return new Response("forbidden", { status: 403 });
return context.next(new Request(req, { body: JSON.stringify(body) }));
};
Netlify user gate (private / team-login-protected projects)
ts
import type { Config, Context } from "@netlify/edge-functions";
export default async (req: Request, context: Context) => {
const user = context.user; // undefined: public project, password protection, netlify dev
if (!user) return new Response("Forbidden", { status: 403 });
const allowed = (Netlify.env.get("ADMIN_USER_IDS") ?? "").split(",");
if (!allowed.includes(user.id)) return new Response("Forbidden", { status: 403 });
// user.email may be undefined, so use it for display only
};
export const config: Config = { path: "/admin/*" };
Post-response work
ts
export default async (req: Request, context: Context) => {
context.waitUntil(fetch("https://analytics.example.com/event", { method: "POST" }));
return new Response("ok");
};

waitUntil does not block the response, but it still counts against the 50 ms CPU limit.

Streaming SSR (.tsx)
tsx
import React from "https://esm.sh/react";
import { renderToReadableStream } from "https://esm.sh/react-dom/server";
import type { Config, Context } from "@netlify/edge-functions";
export default async (req: Request, context: Context) => {
const stream = await renderToReadableStream(
<html><body><h1>Hello {context.geo.country?.name}</h1></body></html>
);
return new Response(stream, { headers: { "Content-Type": "text/html" } });
};
export const config: Config = { path: "/hello" };

Context

PropertyNotes
cookies.get(name) / .set(options) / `.delete(name \options)`set and delete use CookieStore option format; they act on the outgoing response
geocity, country.{code,name}, subdivision.{code,name}, latitude, longitude, timezone, postalCode (format varies)
ipclient IP
next(options?) / next(request, options?)Promise<Response> from the next item in the chain
paramsURLPattern named groups from path (/pets/:name → { name }). For the query string, use request.url
user{ id, email?, expiresAt } or undefined. It is a Netlify account, not Netlify Identity; for Identity, use getUser() from @netlify/identity
waitUntil(promise)runs after the response is sent
requestId, account.id, server.region—
site{ id, name, url }
deploy{ context, id, published, skewProtectionToken }

Netlify adds no client-info headers to the request. Read client info from context instead.

Netlify.context gives the same object, but only inside the handler. Outside it, it is null.

Netlify.env: get(name), has(name), toObject(), and invocation-scoped set and delete. To change variables persistently, use the env API.

Web APIs:

  • console, atob, btoa
  • fetch, Request, Response, URL, File, Blob
  • TextEncoder/TextDecoder (and their Stream variants), Performance
  • Web Crypto: randomUUID, getRandomValues, SubtleCrypto
  • WebSocket
  • setTimeout, clearTimeout, setInterval
  • Streams: ReadableStream, WritableStream, TransformStream
  • URLPattern

Modules:

  • Node built-ins: use the node: prefix (import { randomBytes } from "node:crypto").
  • Deno modules: import by URL, or through an import map.
  • npm packages (beta): import by name. Packages with native binaries or runtime dynamic imports may break.

Declarations

Inline config
KeyValue
pathURLPattern string or array; must start with /
excludedPathURLPattern string or array; must start with /
pattern / excludedPatternregex alternatives to path / excludedPath
methodHTTP method string or array
header`{ name: true \false \"regex" }`; case-insensitive names; multi-value headers match the comma-joined list
onError"fail" (default), "/custom-path", or "bypass"
cache"manual"
ts
export const config: Config = {
path: "/*",
excludedPath: ["/*.css", "/*.js", "/img/*"],
header: { "user-agent": "(iPhone|Android)" },
};

The netlify-agent-category request header maps any user-agent to a client category.

netlify.toml

The toml form supports function, path, excludedPath, pattern, excludedPattern, [edge_functions.header], and cache. Set onError inline. Use toml when you need explicit ordering of several functions on one path.

toml
[[edge_functions]]
path = "/admin"
function = "auth"
[[edge_functions]]
path = "/admin"
function = "injector"
cache = "manual"
[[edge_functions]]
pattern = "/products/(.*)"
excludedPattern = "/products/things/(.*)"
function = "highlight"
[[edge_functions]]
path = "/*"
excludedPath = "/img/*"
function = "common"
[edge_functions.header]
x-required = true
x-forbidden = false

A request to /admin runs auth, then common, then injector.

Order

Netlify walks the following order twice for the request path. The first pass runs only functions without caching configured. The second pass runs only cached functions.

  1. Framework-generated functions declared in a config file.
  2. Your netlify.toml declarations, top to bottom.
  3. Framework- and integration-generated inline declarations.
  4. Your inline declarations, alphabetical by file name.

If the same function is declared both in toml and inline, the configs are merged and treated as step 4; inline wins for duplicate fields.

After the edge functions, redirect rules run, unless a function already returned a Response. A redirect to a new path repeats the whole process for that path.

An edge function declared on the target of a static rewrite does not run for rewritten requests.

Response caching

ts
import type { Config } from "@netlify/edge-functions";
export default async () =>
new Response("Hello world", { headers: { "cache-control": "public, s-maxage=3600" } });

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
8
Repository forks
2
7-day trend score
11

Stars and forks are for the whole GitHub repository, not this skill alone. Refreshed 16 Mar 2026, 7 months ago.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/netlify/context-and-tools/netlify-edge-functions/badge)](https://verified-skill.com/skills/netlify/context-and-tools/netlify-edge-functions)

Something wrong with this skill or its page? Tell us.