Netlify Deployment
Netlify platform expertise — static/SSR deployment, serverless functions, forms, identity, build plugins, redirects, and split testing
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 linesNetlify 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
_redirectsornetlify.tomlfor 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
-backgroundsuffix 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_VERSIONinnetlify.tomlto 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 = trueon proxy rewrites — withoutforce, 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
Related Skills
Porter
Porter is an open-source PaaS that deploys applications directly onto your own cloud provider (AWS, GCP, Azure). It abstracts away Kubernetes complexity, offering a Heroku-like developer experience on your existing infrastructure.
Railway Deployment
Railway platform expertise — instant deployment, managed databases, environment variables, cron jobs, private networking, monorepo support, and templates
Render Deployment
Render platform expertise — web services, static sites, background workers, cron jobs, managed databases, blueprints (IaC), and auto-scaling
SST (Serverless Stack)
SST framework expertise — infrastructure as code for AWS, Lambda functions, Next.js/Remix/Astro deployment, constructs, live Lambda development, and the SST Console
Vercel Deployment
Vercel platform expertise — Next.js deployment, serverless/edge functions, environment variables, preview deployments, domains, and monorepo support
Zeabur
Zeabur is a cloud platform for deploying and scaling web services, databases, and cron jobs with automatic Git integration. It simplifies infrastructure management by providing a unified interface for various services and environments, accelerating development from local to production.