Skip to main content
Technology & EngineeringDeployment Hosting Services354 lines

Netlify Deployment

Netlify platform expertise — static/SSR deployment, serverless functions, forms, identity, build plugins, redirects, and split testing

Quick Summary18 lines
Netlify pioneered the Jamstack architecture: pre-render as much as possible, serve from a CDN, and enhance with serverless functions. The platform treats deployments as atomic and immutable — every deploy is a complete snapshot that can be instantly rolled back. Build plugins extend the pipeline, forms work without a backend, and identity provides authentication out of the box. The platform rewards a "pre-render first, enhance second" mindset.

## Key Points

- **Use `_redirects` or `netlify.toml` for routing** — define redirects declaratively rather than in application code for CDN-level performance.
- **Leverage deploy previews for every PR** — each pull request gets a unique URL for testing; add Lighthouse CI and visual regression checks.
- **Use background functions for long tasks** — name the file with `-background` suffix to get 15-minute execution instead of 10 seconds.
- **Cache aggressively in build plugins** — restore and save heavy dependencies (Prisma, sharp) between builds to cut build times.
- **Set context-specific config** — use `[context.production]` and `[context.deploy-preview]` blocks to vary build commands and environment variables.
- **Enable form spam filtering** — use the honeypot attribute (`netlify-honeypot`) and enable Akismet in the Netlify dashboard.
- **Use instant rollbacks** — if a production deploy breaks, roll back to any previous deploy in seconds from the dashboard.
- **Pin Node.js version** — set `NODE_VERSION` in `netlify.toml` to avoid surprises from default version changes.
- **Putting all logic in a single function** — each function file becomes its own endpoint. Split concerns into separate function files for independent scaling and debugging.
- **Ignoring redirect order** — redirects are matched top-to-bottom. Placing the SPA catch-all (`/* /index.html 200`) before specific redirects will swallow them.
- **Expecting persistent filesystem** — functions run in ephemeral containers. Never write to disk expecting persistence; use a database or Netlify Blobs.
- **Overusing edge functions for heavy computation** — edge functions have limited CPU time and memory. Keep them lightweight; offload heavy work to background functions.
skilldb get deployment-hosting-services-skills/netlifyFull skill: 354 lines
Paste into your CLAUDE.md or agent config

Netlify Deployment

Core Philosophy

Netlify pioneered the Jamstack architecture: pre-render as much as possible, serve from a CDN, and enhance with serverless functions. The platform treats deployments as atomic and immutable — every deploy is a complete snapshot that can be instantly rolled back. Build plugins extend the pipeline, forms work without a backend, and identity provides authentication out of the box. The platform rewards a "pre-render first, enhance second" mindset.

Setup

Project Configuration

// netlify.toml — primary configuration file at repo root
// [build]
//   command = "npm run build"
//   publish = "dist"
//   functions = "netlify/functions"
//
// [build.environment]
//   NODE_VERSION = "20"
//   NPM_FLAGS = "--legacy-peer-deps"
//
// [[redirects]]
//   from = "/api/*"
//   to = "/.netlify/functions/:splat"
//   status = 200
//
// [[headers]]
//   for = "/*"
//   [headers.values]
//     X-Frame-Options = "DENY"
//     X-Content-Type-Options = "nosniff"

// netlify.toml context overrides
// [context.production]
//   command = "npm run build:prod"
//   environment = { API_URL = "https://api.example.com" }
//
// [context.deploy-preview]
//   command = "npm run build:preview"
//   environment = { API_URL = "https://staging-api.example.com" }

CLI Setup

// Install CLI and link site
// $ npm i -g netlify-cli
// $ netlify login
// $ netlify link
// $ netlify dev  — local development server with functions

// Deploy draft
// $ netlify deploy

// Deploy to production
// $ netlify deploy --prod

Key Techniques

Serverless Functions

// netlify/functions/users.ts
import type { Handler, HandlerEvent, HandlerContext } from "@netlify/functions";

interface User {
  id: string;
  name: string;
  email: string;
}

const handler: Handler = async (event: HandlerEvent, context: HandlerContext) => {
  const { httpMethod, queryStringParameters, body } = event;

  if (httpMethod === "GET") {
    const page = parseInt(queryStringParameters?.page ?? "1", 10);
    const users = await fetchUsers(page);

    return {
      statusCode: 200,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(users),
    };
  }

  if (httpMethod === "POST") {
    const data: Partial<User> = JSON.parse(body ?? "{}");
    const user = await createUser(data);

    return {
      statusCode: 201,
      body: JSON.stringify(user),
    };
  }

  return { statusCode: 405, body: "Method Not Allowed" };
};

export { handler };

Background Functions

// netlify/functions/process-upload-background.ts
// Suffix "-background" gives 15 minutes instead of 10 seconds
import type { Handler } from "@netlify/functions";

const handler: Handler = async (event) => {
  const { fileId, userId } = JSON.parse(event.body ?? "{}");

  // Long-running process — up to 15 minutes
  await downloadFile(fileId);
  await processFile(fileId);
  await notifyUser(userId, fileId);

  return { statusCode: 200, body: "Processing complete" };
};

export { handler };

Scheduled Functions

// netlify/functions/daily-report.ts
import type { Handler } from "@netlify/functions";
import { schedule } from "@netlify/functions";

const handler: Handler = async (event) => {
  const report = await generateDailyReport();
  await sendReportEmail(report);

  return { statusCode: 200, body: "Report sent" };
};

// Runs every day at 8am UTC
export default schedule("0 8 * * *", handler);

Edge Functions

// netlify/edge-functions/geolocation.ts
import type { Context } from "@netlify/edge-functions";

export default async (request: Request, context: Context) => {
  const { country, city, latitude, longitude } = context.geo;

  // Rewrite to country-specific page
  if (country?.code === "DE") {
    return context.rewrite("/de" + new URL(request.url).pathname);
  }

  // Or modify the response
  const response = await context.next();
  response.headers.set("x-geo-country", country?.code ?? "unknown");
  return response;
};

// Configure path matching in netlify.toml:
// [[edge_functions]]
//   path = "/*"
//   function = "geolocation"

Forms

// HTML form with Netlify detection — no backend needed
// <form name="contact" method="POST" data-netlify="true" netlify-honeypot="bot-field">
//   <input type="hidden" name="form-name" value="contact" />
//   <p class="hidden"><input name="bot-field" /></p>
//   <input type="text" name="name" required />
//   <input type="email" name="email" required />
//   <textarea name="message" required></textarea>
//   <button type="submit">Send</button>
// </form>

// React form submission
async function submitForm(data: { name: string; email: string; message: string }) {
  const response = await fetch("/", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      "form-name": "contact",
      ...data,
    }).toString(),
  });

  if (!response.ok) throw new Error("Form submission failed");
  return response;
}

Build Plugins

// plugins/cache-prisma/index.ts — custom build plugin
import type { NetlifyPlugin } from "@netlify/build";

const plugin: NetlifyPlugin = {
  onPreBuild: async ({ utils }) => {
    // Restore cached Prisma engine
    const success = await utils.cache.restore("node_modules/.prisma");
    if (success) {
      console.log("Prisma cache restored");
    } else {
      console.log("No Prisma cache found, will generate");
    }
  },

  onPostBuild: async ({ utils }) => {
    // Cache Prisma engine for next build
    await utils.cache.save("node_modules/.prisma");
    console.log("Prisma cache saved");
  },

  onError: ({ utils }) => {
    utils.build.failPlugin("Build plugin encountered an error");
  },
};

export default plugin;

// Register in netlify.toml:
// [[plugins]]
//   package = "./plugins/cache-prisma"

Redirects and Rewrites

// netlify.toml — redirect rules (processed in order, first match wins)

// [[redirects]]
//   from = "/old-blog/*"
//   to = "/blog/:splat"
//   status = 301
//
// # Proxy to external API (rewrite, no redirect)
// [[redirects]]
//   from = "/api/v1/*"
//   to = "https://api.backend.com/v1/:splat"
//   status = 200
//   force = true
//   headers = {X-Custom-Header = "netlify-proxy"}
//
// # Country-based redirect
// [[redirects]]
//   from = "/*"
//   to = "/de/:splat"
//   status = 302
//   conditions = {Country = ["DE", "AT", "CH"]}
//
// # SPA fallback — must be last
// [[redirects]]
//   from = "/*"
//   to = "/index.html"
//   status = 200

Split Testing (A/B)

// Configure branch-based split testing via netlify.toml or UI
// Deploys from different branches are served to a percentage of users

// In your code, detect the branch via cookie
function getVariant(): "control" | "experiment" {
  const cookie = document.cookie
    .split("; ")
    .find((row) => row.startsWith("nf_ab="));

  if (cookie) {
    return cookie.split("=")[1] as "control" | "experiment";
  }
  return "control";
}

// Track conversion
async function trackConversion(variant: string) {
  await fetch("/api/analytics", {
    method: "POST",
    body: JSON.stringify({ variant, event: "signup" }),
  });
}

Identity (Authentication)

// Initialize Netlify Identity widget
// import netlifyIdentity from "netlify-identity-widget";

// netlifyIdentity.init();

// Sign up / log in
// netlifyIdentity.open("signup");
// netlifyIdentity.open("login");

// Listen for events
// netlifyIdentity.on("login", (user) => {
//   console.log("Logged in:", user.email);
//   const token = user.token.access_token;
// });

// Protect a function with identity
// netlify/functions/protected.ts
import type { Handler } from "@netlify/functions";

const handler: Handler = async (event, context) => {
  const { identity, user } = context.clientContext ?? {};

  if (!user) {
    return { statusCode: 401, body: JSON.stringify({ error: "Not authenticated" }) };
  }

  const userData = await fetchUserData(user.sub);
  return {
    statusCode: 200,
    body: JSON.stringify({ user: userData }),
  };
};

export { handler };

Best Practices

  • Use _redirects or netlify.toml for routing — define redirects declaratively rather than in application code for CDN-level performance.
  • Leverage deploy previews for every PR — each pull request gets a unique URL for testing; add Lighthouse CI and visual regression checks.
  • Use background functions for long tasks — name the file with -background suffix to get 15-minute execution instead of 10 seconds.
  • Cache aggressively in build plugins — restore and save heavy dependencies (Prisma, sharp) between builds to cut build times.
  • Set context-specific config — use [context.production] and [context.deploy-preview] blocks to vary build commands and environment variables.
  • Enable form spam filtering — use the honeypot attribute (netlify-honeypot) and enable Akismet in the Netlify dashboard.
  • Use instant rollbacks — if a production deploy breaks, roll back to any previous deploy in seconds from the dashboard.
  • Pin Node.js version — set NODE_VERSION in netlify.toml to avoid surprises from default version changes.

Anti-Patterns

  • Putting all logic in a single function — each function file becomes its own endpoint. Split concerns into separate function files for independent scaling and debugging.
  • Ignoring redirect order — redirects are matched top-to-bottom. Placing the SPA catch-all (/* /index.html 200) before specific redirects will swallow them.
  • Using environment variables at build time for secrets — build logs may expose them. Use function-level environment variables for secrets and never prefix with framework-specific public prefixes.
  • Expecting persistent filesystem — functions run in ephemeral containers. Never write to disk expecting persistence; use a database or Netlify Blobs.
  • Overusing edge functions for heavy computation — edge functions have limited CPU time and memory. Keep them lightweight; offload heavy work to background functions.
  • Skipping force = true on proxy rewrites — without force, Netlify will serve a static file if one matches the path, ignoring the rewrite rule.
  • Not testing locally with netlify dev — the CLI accurately simulates redirects, functions, and environment variables. Always test before pushing.

Install this skill directly: skilldb add deployment-hosting-services-skills

Get CLI access →