How to deploy Node.js app to Netlify functions
Deploying Node.js applications to Netlify Functions provides serverless API endpoints with automatic deployment, Git integration, and edge distribution. With over 12 years of Node.js experience since 2014 and as the creator of CoreUI, I’ve deployed numerous serverless functions to Netlify. Netlify Functions run on AWS Lambda providing scalable backend logic without server management with automatic HTTPS and custom domains. This approach works perfectly for API routes, form handlers, webhooks, and backend services with instant deployment on Git push.
Create functions in netlify/functions directory and deploy via Git for automatic serverless Node.js deployment.
Basic Netlify function:
// netlify/functions/hello.js
exports.handler = async (event, context) => {
return {
statusCode: 200,
body: JSON.stringify({
message: 'Hello from Netlify Functions!',
timestamp: new Date().toISOString()
})
}
}
// Accessible at: /.netlify/functions/hello
REST API endpoints:
// netlify/functions/users.js
exports.handler = async (event, context) => {
const { httpMethod, path, body, queryStringParameters } = event
// Enable CORS
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE'
}
// Handle preflight
if (httpMethod === 'OPTIONS') {
return { statusCode: 200, headers, body: '' }
}
try {
switch (httpMethod) {
case 'GET':
// Get user by ID from query params
const userId = queryStringParameters?.id
return {
statusCode: 200,
headers,
body: JSON.stringify({
id: userId,
name: 'John Doe',
email: '[email protected]'
})
}
case 'POST':
// Create user
const userData = JSON.parse(body)
return {
statusCode: 201,
headers,
body: JSON.stringify({
success: true,
user: userData
})
}
case 'PUT':
// Update user
const updateData = JSON.parse(body)
return {
statusCode: 200,
headers,
body: JSON.stringify({
success: true,
updated: updateData
})
}
case 'DELETE':
// Delete user
return {
statusCode: 204,
headers,
body: ''
}
default:
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: 'Method not allowed' })
}
}
} catch (error) {
return {
statusCode: 500,
headers,
body: JSON.stringify({ error: error.message })
}
}
}
Environment variables:
// netlify/functions/config.js
exports.handler = async (event, context) => {
// Access environment variables
const apiKey = process.env.API_KEY
const dbUrl = process.env.DATABASE_URL
return {
statusCode: 200,
body: JSON.stringify({
environment: process.env.CONTEXT, // production, deploy-preview, branch-deploy
hasApiKey: !!apiKey,
hasDbUrl: !!dbUrl
})
}
}
// Set via Netlify UI: Site settings > Environment variables
// Or in netlify.toml
netlify.toml configuration:
[build]
functions = "netlify/functions"
publish = "dist"
[functions]
node_bundler = "esbuild"
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
[context.production.environment]
NODE_ENV = "production"
[context.deploy-preview.environment]
NODE_ENV = "development"
Database integration:
// netlify/functions/db-users.js
import { Client } from 'pg'
exports.handler = async (event, context) => {
const client = new Client({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false }
})
try {
await client.connect()
const result = await client.query('SELECT * FROM users LIMIT 10')
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
users: result.rows,
count: result.rowCount
})
}
} catch (error) {
console.error('Database error:', error)
return {
statusCode: 500,
body: JSON.stringify({ error: 'Database query failed' })
}
} finally {
await client.end()
}
}
// package.json
{
"dependencies": {
"pg": "^8.11.3"
}
}
Scheduled functions:
// netlify/functions/scheduled-task.js
import fetch from 'node-fetch'
exports.handler = async (event, context) => {
console.log('Scheduled function executed at:', new Date().toISOString())
try {
// Perform scheduled task
const response = await fetch('https://api.example.com/sync')
const data = await response.json()
return {
statusCode: 200,
body: JSON.stringify({
success: true,
data: data
})
}
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: error.message })
}
}
}
// netlify.toml
[[functions]]
path = "scheduled-task"
schedule = "0 0 * * *" # Daily at midnight
Background functions:
// netlify/functions/background-process.js
exports.handler = async (event, context) => {
// Start background task
setTimeout(() => {
console.log('Background task completed')
// This runs after function returns
}, 5000)
// Return immediately
return {
statusCode: 202,
body: JSON.stringify({
message: 'Background task started'
})
}
}
// netlify.toml
[[functions]]
path = "background-process"
[functions.background-process]
background = true
Form handling:
// netlify/functions/contact-form.js
import nodemailer from 'nodemailer'
exports.handler = async (event, context) => {
if (event.httpMethod !== 'POST') {
return { statusCode: 405, body: 'Method Not Allowed' }
}
try {
const { name, email, message } = JSON.parse(event.body)
// Validate input
if (!name || !email || !message) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Missing required fields' })
}
}
// Send email
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
})
await transporter.sendMail({
from: email,
to: '[email protected]',
subject: `Contact form: ${name}`,
text: message
})
return {
statusCode: 200,
body: JSON.stringify({ success: true })
}
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: error.message })
}
}
}
Authentication middleware:
// netlify/functions/utils/auth.js
export function verifyToken(event) {
const token = event.headers.authorization?.replace('Bearer ', '')
if (!token) {
return { authorized: false, error: 'No token provided' }
}
try {
// Verify JWT token
const decoded = jwt.verify(token, process.env.JWT_SECRET)
return { authorized: true, user: decoded }
} catch (error) {
return { authorized: false, error: 'Invalid token' }
}
}
// netlify/functions/protected.js
import { verifyToken } from './utils/auth'
exports.handler = async (event, context) => {
const auth = verifyToken(event)
if (!auth.authorized) {
return {
statusCode: 401,
body: JSON.stringify({ error: auth.error })
}
}
return {
statusCode: 200,
body: JSON.stringify({
message: 'Protected data',
user: auth.user
})
}
}
Package.json:
{
"name": "netlify-functions",
"version": "1.0.0",
"scripts": {
"dev": "netlify dev",
"build": "netlify build",
"deploy": "netlify deploy --prod"
},
"dependencies": {
"node-fetch": "^2.7.0",
"pg": "^8.11.3"
},
"devDependencies": {
"netlify-cli": "^17.10.1"
}
}
Local development:
# Install Netlify CLI
npm install -g netlify-cli
# Login
netlify login
# Init project
netlify init
# Run locally (with functions)
netlify dev
# Test function locally
curl http://localhost:8888/.netlify/functions/hello
# Deploy to production
netlify deploy --prod
GitHub integration:
# Connect repository via Netlify UI
# Automatic deployments on:
# - Push to main branch (production)
# - Pull requests (deploy previews)
# Build settings in Netlify UI:
# Build command: npm run build
# Publish directory: dist
# Functions directory: netlify/functions
Best Practice Note
Keep functions in netlify/functions directory for automatic deployment. Use environment variables for secrets via Netlify UI. Functions timeout after 10 seconds (26 seconds for background). Enable CORS headers for API endpoints. Use redirects in netlify.toml for cleaner URLs. Implement proper error handling and logging. Keep functions lightweight and focused. Use background functions for long-running tasks. This is how we deploy serverless functions with Netlify—automatic deployment from Git, environment configuration, and scalable backend logic without server management.



