Netlify - complete guide to the Jamstack platform
Remember when deploying a website meant FTPing files to a server and praying nothing broke? Netlify made that ancient history. With a simple git push, your site builds automatically and deploys to a global CDN in seconds. No servers to manage, no complex CI/CD pipelines to configure, no DevOps degree required. Just push your code and watch it go live.
This guide covers everything from your first deployment to advanced patterns like edge functions, serverless APIs, and sophisticated A/B testing. Whether you're hosting a personal blog or building a production-grade SaaS application, you'll find the tools and techniques you need.
What is Netlify?
Netlify is a pioneering platform that revolutionized the way websites are built and hosted. Founded in 2014 by Mathias Biilmann and Christian Bach, Netlify introduced and popularized the "Jamstack" concept - an architecture based on JavaScript, APIs, and Markup that separates frontend from backend.
Netlify offers a complete ecosystem for building modern web applications: automatic CI/CD from GitHub/GitLab/Bitbucket, global CDN, serverless functions, edge computing, backend-free forms, user authentication, and much more. All of this is accessible from a single dashboard, without the need to manage servers.
The platform gained popularity thanks to its "developer experience first" philosophy - deploying a site is literally git push, and the new version is automatically built and published to the global CDN network. This simplicity has attracted millions of developers and hundreds of thousands of companies worldwide.
History and industry impact
Netlify was founded as an answer to the complexity of traditional web hosting. Mathias Biilmann, formerly CTO of BitBalloon, noticed that developers were spending too much time configuring servers instead of building products.
In 2015, Netlify introduced the term "Jamstack", promoting an architecture where sites are pre-rendered and served from CDN, while dynamic functionality is delivered through APIs and serverless functions. This philosophy changed how people think about web development.
Today, Netlify hosts millions of sites, including projects like React, Vue.js, Kubernetes, and many others. The company has raised over $200 million in funding and is recognized as a leader in the Jamstack platform category.
Why choose Netlify?
Key advantages
- Instant deploys - Site on CDN in seconds, atomic deploys
- Deploy Previews - Every PR gets a unique preview URL
- Serverless Functions - Backend without managing servers
- Edge Functions - Logic at the edge, minimal latency
- Netlify Forms - Forms without backend code
- Split Testing - Native A/B testing
- Rollbacks - One click to previous version
- Branch deploys - Every branch has its own URL
Netlify vs Vercel vs Cloudflare Pages
| Feature | Netlify | Vercel | Cloudflare Pages |
|---|---|---|---|
| Price (hobby) | Free tier | Free tier | Free tier |
| CI/CD | ✅ Built-in | ✅ Built-in | ✅ Built-in |
| Serverless Functions | ✅ | ✅ | ✅ Workers |
| Edge Functions | ✅ | ✅ | ✅ (native) |
| Forms | ✅ Native | ❌ | ❌ |
| Identity/Auth | ✅ Native | ❌ | ❌ |
| Split Testing | ✅ | ❌ | ❌ |
| Next.js support | ✅ | ✅ Best | ✅ |
| Build minutes (free) | 300/mo | 6000/mo | Unlimited |
| Bandwidth (free) | 100GB | 100GB | Unlimited |
| Analytics | ✅ Paid | ✅ Paid | ✅ Free |
Getting started
Method 1: deploy from Git (recommended)
# 1. Connect repository
# Go to app.netlify.com → New site from Git → GitHub/GitLab/Bitbucket
# 2. Select repository and branch
# 3. Configure build
Build command: npm run build
Publish directory: dist (or build, out, .next, public)
# 4. Deploy!
# Every push automatically triggers a new deployMethod 2: Netlify CLI
# Installation
npm install -g netlify-cli
# Login
netlify login
# Initialize project (in project folder)
netlify init
# Production deploy
netlify deploy --prod
# Deploy preview (test)
netlify deploy
# Development server with Functions
netlify devMethod 3: drag and drop
# Simplest way:
# 1. Build site locally: npm run build
# 2. Drag build folder to app.netlify.com/drop
# 3. Done!Project configuration
netlify.toml (main configuration file)
# netlify.toml
# Basic build configuration
[build]
command = "npm run build"
publish = "dist"
functions = "netlify/functions"
edge_functions = "netlify/edge-functions"
# Environment variables
[build.environment]
NODE_VERSION = "20"
NPM_FLAGS = "--legacy-peer-deps"
# Production context
[context.production]
command = "npm run build"
environment = { NODE_ENV = "production" }
# Staging context (branch: staging)
[context.staging]
command = "npm run build:staging"
environment = { NODE_ENV = "staging" }
# Deploy-preview context (PRs)
[context.deploy-preview]
command = "npm run build:preview"
# Branch-deploy context (other branches)
[context.branch-deploy]
command = "npm run build:dev"
# HTTP Headers
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
[[headers]]
for = "/static/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
# Redirects
[[redirects]]
from = "/old-page"
to = "/new-page"
status = 301
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
# SPA fallback
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
# Proxy to external API
[[redirects]]
from = "/external-api/*"
to = "https://api.external.com/:splat"
status = 200
force = true
# Edge Functions routing
[[edge_functions]]
function = "geolocation"
path = "/api/location"
[[edge_functions]]
function = "auth"
path = "/dashboard/*"
# Plugins
[[plugins]]
package = "@netlify/plugin-nextjs"
[[plugins]]
package = "netlify-plugin-sitemap"_redirects (alternative)
# Simpler syntax for redirects
# 301 Redirect
/old-path /new-path 301
# Redirect with wildcard
/blog/* /articles/:splat 301
# Proxy (preserves original URL)
/api/* /.netlify/functions/:splat 200
# SPA fallback
/* /index.html 200
# Conditional redirect (geo)
/pricing /pricing-us 200 Country=us
/pricing /pricing-eu 200 Country=de,fr,gb
# Conditional redirect (language)
/* /pl/:splat 200 Language=pl
/* /en/:splat 200 Language=enNetlify Functions
Basic function
// netlify/functions/hello.ts
import type { Handler, HandlerEvent, HandlerContext } from '@netlify/functions'
export const handler: Handler = async (
event: HandlerEvent,
context: HandlerContext
) => {
const { name = 'World' } = event.queryStringParameters || {}
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
message: `Hello, ${name}!`,
timestamp: new Date().toISOString()
})
}
}
// Call: /.netlify/functions/hello?name=JohnPOST request with body
// netlify/functions/create-user.ts
import type { Handler } from '@netlify/functions'
interface CreateUserBody {
email: string
name: string
password: string
}
export const handler: Handler = async (event) => {
// Handle CORS preflight
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
},
body: ''
}
}
if (event.httpMethod !== 'POST') {
return {
statusCode: 405,
body: JSON.stringify({ error: 'Method not allowed' })
}
}
try {
const body: CreateUserBody = JSON.parse(event.body || '{}')
// Validation
if (!body.email || !body.name || !body.password) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Missing required fields' })
}
}
// User creation logic here
// e.g., save to database, send email
return {
statusCode: 201,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
success: true,
user: {
id: 'user_' + Date.now(),
email: body.email,
name: body.name
}
})
}
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' })
}
}
}Scheduled Functions (CRON)
// netlify/functions/daily-report.ts
import type { Handler } from '@netlify/functions'
export const handler: Handler = async (event, context) => {
// Check if this is a scheduled invocation
const isScheduled = event.headers['x-netlify-scheduled']
if (!isScheduled) {
return {
statusCode: 403,
body: 'This function can only be triggered by schedule'
}
}
// Report logic
console.log('Running daily report at', new Date().toISOString())
// e.g., send email report
await sendDailyReport()
return {
statusCode: 200,
body: 'Daily report sent'
}
}
// Configuration in netlify.toml:
// [functions."daily-report"]
// schedule = "0 8 * * *" # Daily at 8:00 UTCBackground Functions
// netlify/functions/process-webhook-background.ts
import type { BackgroundHandler } from '@netlify/functions'
// Background functions can run up to 15 minutes
export const handler: BackgroundHandler = async (event, context) => {
const payload = JSON.parse(event.body || '{}')
// Long-running operation
console.log('Processing webhook:', payload.id)
// e.g., processing large file
// e.g., sending many emails
// e.g., syncing with external API
for (let i = 0; i < payload.items.length; i++) {
await processItem(payload.items[i])
console.log(`Processed ${i + 1}/${payload.items.length}`)
}
// Background functions don't return a response
// Caller immediately gets 202 Accepted
}
// Filename must end with -background.tsFunction with database
// netlify/functions/users.ts
import type { Handler } from '@netlify/functions'
import { MongoClient } from 'mongodb'
const client = new MongoClient(process.env.MONGODB_URI!)
export const handler: Handler = async (event) => {
try {
await client.connect()
const db = client.db('myapp')
const users = db.collection('users')
switch (event.httpMethod) {
case 'GET': {
const allUsers = await users.find({}).toArray()
return {
statusCode: 200,
body: JSON.stringify(allUsers)
}
}
case 'POST': {
const body = JSON.parse(event.body || '{}')
const result = await users.insertOne({
...body,
createdAt: new Date()
})
return {
statusCode: 201,
body: JSON.stringify({ id: result.insertedId })
}
}
default:
return {
statusCode: 405,
body: 'Method not allowed'
}
}
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: 'Database error' })
}
}
}Edge Functions
Basic Edge Function
// netlify/edge-functions/hello.ts
import type { Context } from "@netlify/edge-functions"
export default async (request: Request, context: Context) => {
const url = new URL(request.url)
const name = url.searchParams.get('name') || 'World'
return new Response(`Hello from the Edge, ${name}!`, {
headers: {
'Content-Type': 'text/plain',
'X-Edge-Location': context.geo.city || 'unknown'
}
})
}
// Configuration in netlify.toml:
// [[edge_functions]]
// function = "hello"
// path = "/api/hello"Geolocation at the Edge
// netlify/edge-functions/geolocation.ts
import type { Context } from "@netlify/edge-functions"
export default async (request: Request, context: Context) => {
const { geo } = context
return Response.json({
city: geo.city,
country: geo.country?.code,
countryName: geo.country?.name,
region: geo.subdivision?.code,
timezone: geo.timezone,
latitude: geo.latitude,
longitude: geo.longitude
})
}Edge Function as Middleware
// netlify/edge-functions/auth-middleware.ts
import type { Context } from "@netlify/edge-functions"
export default async (request: Request, context: Context) => {
const authHeader = request.headers.get('Authorization')
// Check token
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return new Response('Unauthorized', {
status: 401,
headers: { 'WWW-Authenticate': 'Bearer' }
})
}
const token = authHeader.substring(7)
try {
// Token verification
const isValid = await verifyToken(token)
if (!isValid) {
return new Response('Invalid token', { status: 401 })
}
// Continue to original page
return context.next()
} catch (error) {
return new Response('Authentication error', { status: 500 })
}
}
// Configuration in netlify.toml:
// [[edge_functions]]
// function = "auth-middleware"
// path = "/dashboard/*"A/B Testing at the Edge
// netlify/edge-functions/ab-test.ts
import type { Context } from "@netlify/edge-functions"
export default async (request: Request, context: Context) => {
// Check if user already has an assigned version
const cookies = request.headers.get('Cookie') || ''
const existingVariant = cookies.match(/ab-variant=(\w+)/)?.[1]
// Assign randomly if none
const variant = existingVariant || (Math.random() < 0.5 ? 'A' : 'B')
// Redirect to appropriate version
const url = new URL(request.url)
if (variant === 'B') {
url.pathname = url.pathname.replace('/pricing', '/pricing-new')
}
const response = await context.rewrite(url.toString())
// Set cookie if new user
if (!existingVariant) {
response.headers.set(
'Set-Cookie',
`ab-variant=${variant}; Path=/; Max-Age=2592000`
)
}
// Add header for analytics
response.headers.set('X-AB-Variant', variant)
return response
}Netlify Forms
Basic HTML form
<!-- Form with data-netlify="true" -->
<form name="contact" method="POST" data-netlify="true">
<input type="hidden" name="form-name" value="contact" />
<label>
Name:
<input type="text" name="name" required />
</label>
<label>
Email:
<input type="email" name="email" required />
</label>
<label>
Message:
<textarea name="message" required></textarea>
</label>
<button type="submit">Send</button>
</form>Form with AJAX (React)
// components/ContactForm.tsx
import { useState } from 'react'
export function ContactForm() {
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setStatus('loading')
const form = e.currentTarget
const formData = new FormData(form)
try {
const response = await fetch('/', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(formData as any).toString()
})
if (response.ok) {
setStatus('success')
form.reset()
} else {
setStatus('error')
}
} catch (error) {
setStatus('error')
}
}
return (
<form
name="contact"
method="POST"
data-netlify="true"
onSubmit={handleSubmit}
>
<input type="hidden" name="form-name" value="contact" />
<input type="text" name="name" placeholder="Name" required />
<input type="email" name="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required />
<button type="submit" disabled={status === 'loading'}>
{status === 'loading' ? 'Sending...' : 'Send'}
</button>
{status === 'success' && <p>Thank you for your message!</p>}
{status === 'error' && <p>An error occurred. Please try again.</p>}
</form>
)
}Form with honeypot (spam protection)
<form name="contact" method="POST" data-netlify="true" data-netlify-honeypot="bot-field">
<input type="hidden" name="form-name" value="contact" />
<!-- Honeypot - hidden field for bots -->
<p style="display: none;">
<label>
Don't fill this out:
<input name="bot-field" />
</label>
</p>
<input type="text" name="name" required />
<input type="email" name="email" required />
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>Netlify Identity
Configuration
// Install Netlify Identity Widget
// npm install netlify-identity-widget
import netlifyIdentity from 'netlify-identity-widget'
// Initialize
netlifyIdentity.init()
// Open login modal
function openLogin() {
netlifyIdentity.open('login')
}
// Open signup modal
function openSignup() {
netlifyIdentity.open('signup')
}
// Logout
function logout() {
netlifyIdentity.logout()
}
// Listen to events
netlifyIdentity.on('login', (user) => {
console.log('User logged in:', user)
netlifyIdentity.close()
})
netlifyIdentity.on('logout', () => {
console.log('User logged out')
})
// Get current user
const user = netlifyIdentity.currentUser()React Hook for Identity
// hooks/useAuth.ts
import { useState, useEffect, createContext, useContext } from 'react'
import netlifyIdentity from 'netlify-identity-widget'
interface User {
id: string
email: string
user_metadata: {
full_name?: string
avatar_url?: string
}
app_metadata: {
roles?: string[]
}
token: {
access_token: string
expires_at: number
}
}
interface AuthContextType {
user: User | null
login: () => void
logout: () => void
signup: () => void
isAuthenticated: boolean
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null)
useEffect(() => {
netlifyIdentity.init()
netlifyIdentity.on('login', (user) => {
setUser(user as User)
netlifyIdentity.close()
})
netlifyIdentity.on('logout', () => {
setUser(null)
})
// Check if already logged in
const currentUser = netlifyIdentity.currentUser()
if (currentUser) {
setUser(currentUser as User)
}
}, [])
return (
<AuthContext.Provider
value={{
user,
login: () => netlifyIdentity.open('login'),
logout: () => netlifyIdentity.logout(),
signup: () => netlifyIdentity.open('signup'),
isAuthenticated: !!user
}}
>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}Netlify CLI
Installation and setup
# Global installation
npm install -g netlify-cli
# Login
netlify login
# Status
netlify status
# Link existing site
netlify link
# Create new site
netlify sites:createLocal development
# Development server with Functions and Edge Functions
netlify dev
# With specific port
netlify dev --port 8888
# With live reload
netlify dev --live
# Functions only
netlify functions:serve
# Test single function
netlify functions:invoke hello --payload '{"name": "John"}'Deploy commands
# Deploy preview (draft)
netlify deploy
# Production deploy
netlify deploy --prod
# Deploy from specific folder
netlify deploy --dir=build --prod
# Deploy with message
netlify deploy --prod --message "Release v1.2.3"
# Deploy and open in browser
netlify deploy --prod --openEnvironment variables
# List variables
netlify env:list
# Set variable
netlify env:set API_KEY "secret-key"
# Remove variable
netlify env:unset API_KEY
# Import from .env file
netlify env:import .envBuild Plugins
Popular plugins
# netlify.toml
# Next.js support
[[plugins]]
package = "@netlify/plugin-nextjs"
# Sitemap generator
[[plugins]]
package = "netlify-plugin-sitemap"
[plugins.inputs]
buildDir = "dist"
# Lighthouse CI
[[plugins]]
package = "@netlify/plugin-lighthouse"
[plugins.inputs]
fail_on_grade = "B"
# Cache dependencies
[[plugins]]
package = "netlify-plugin-cache"
[plugins.inputs]
paths = [
"node_modules",
".cache"
]Framework integrations
Next.js
# Automatic configuration
# Netlify detects Next.js and configures automatically
# netlify.toml
[build]
command = "npm run build"
publish = ".next"
[[plugins]]
package = "@netlify/plugin-nextjs"Astro
# Install adapter
npm install @astrojs/netlify
# astro.config.mjs
import { defineConfig } from 'astro/config'
import netlify from '@astrojs/netlify'
export default defineConfig({
output: 'server', // or 'hybrid'
adapter: netlify()
})SvelteKit
# Install adapter
npm install -D @sveltejs/adapter-netlify
# svelte.config.js
import adapter from '@sveltejs/adapter-netlify'
export default {
kit: {
adapter: adapter()
}
}Pricing
Free Tier
| Resource | Limit |
|---|---|
| Bandwidth | 100GB/month |
| Build minutes | 300/month |
| Concurrent builds | 1 |
| Serverless Functions | 125K invocations/month |
| Edge Functions | 3M invocations/month |
| Forms | 100 submissions/month |
| Identity | 1000 users |
| Sites | Unlimited |
Pro ($19/month per member)
| Resource | Limit |
|---|---|
| Bandwidth | 1TB/month |
| Build minutes | 25,000/month |
| Concurrent builds | 3 |
| Serverless Functions | 125K (included), then $25/1M |
| Forms | 1000 submissions/month |
| Analytics | Included |
| Background Functions | Included |
| Priority support | Included |
Business ($99/month per member)
| Resource | Limit |
|---|---|
| Bandwidth | 1.5TB/month |
| Build minutes | 30,000/month |
| Concurrent builds | 5 |
| SAML SSO | Included |
| Audit logs | Included |
| 99.99% SLA | Included |
FAQ - frequently asked questions
Is Netlify free?
Yes, Netlify offers a generous free plan with 100GB bandwidth, 300 build minutes, and basic features. For most hobby projects and small business sites, the Free tier is sufficient.
How do I configure a custom domain?
In the Netlify dashboard: Site settings → Domain management → Add custom domain. Netlify automatically configures SSL certificate (Let's Encrypt). You need to add DNS records at your registrar pointing to Netlify.
How do Serverless Functions vs Edge Functions differ?
Serverless Functions run in AWS Lambda, have access to Node.js, timeout up to 26 seconds (background: 15 min), ideal for APIs and integrations.
Edge Functions run on Deno in the global edge network, have minimal latency (<50ms), 50ms timeout, ideal for personalization, A/B tests, and middleware.
Can I use Netlify for WordPress?
Not directly. Netlify is for static/Jamstack sites. However, you can use WordPress as a headless CMS and generate a static site (e.g., Gatsby, Next.js) deployed to Netlify.
How do I debug Netlify Functions?
- Locally:
netlify devruns functions with hot reload - Logs: Functions → Function logs in Netlify dashboard
netlify functions:invoketo test from CLI- Add
console.log()- everything goes to logs
Does Netlify support server-side rendering (SSR)?
Yes, through Next.js (with @netlify/plugin-nextjs), Nuxt, SvelteKit, and other frameworks. SSR runs through Serverless Functions. For better performance, consider Edge SSR.
How do I migrate from Vercel to Netlify?
- Check
vercel.json→ convert tonetlify.toml - API routes → Netlify Functions
- Middleware → Edge Functions
- Connect repo to Netlify
- Update environment variables
- Redirect domain
Summary
Netlify is the pioneering Jamstack platform that revolutionized web development. It offers a complete ecosystem: automatic CI/CD, global CDN, Serverless and Edge Functions, forms, authentication, and much more - all from a single dashboard.
Main advantages of Netlify:
- Instant deploys - site on CDN in seconds
- Deploy Previews - every PR has its own URL
- Zero config - automatic framework detection
- Edge computing - personalization close to the user
- Forms & Identity - backend without backend
- Generous free tier - 100GB bandwidth, 300 build minutes
Whether for a portfolio, business site, SaaS, or enterprise application - Netlify provides reliable infrastructure and excellent developer experience.