Hosting an API is not the same as hosting a website. Websites serve HTML to browsers. APIs serve JSON to machines — mobile apps, frontend SPAs, third-party integrations, IoT devices, and other servers. The traffic patterns are different (higher request rates, smaller payloads, stateless), the security requirements are different (API keys, JWTs, CORS), and the monitoring needs are different (p99 latency matters more than page load time). This guide covers the complete API hosting stack: deployment, reverse proxy configuration, security hardening, rate limiting, authentication, monitoring, and capacity planning.
MassiveGRID Ubuntu VPS includes: Ubuntu 24.04 LTS pre-installed · Proxmox HA cluster with automatic failover · Ceph 3x replicated NVMe storage · Independent CPU/RAM/storage scaling · 12 Tbps DDoS protection · 4 global datacenter locations · 100% uptime SLA · 24/7 human support rated 9.5/10
Deploy a self-managed VPS — from $1.99/mo
Need dedicated resources? — from $19.80/mo
Want fully managed hosting? — we handle everything
API Hosting vs Website Hosting
Before diving into configuration, understand the fundamental differences between hosting a REST API and hosting a traditional website:
| Aspect | Website | REST API |
|---|---|---|
| Response format | HTML, CSS, JS, images | JSON (small payloads, 1-50 KB) |
| Client type | Browsers | Mobile apps, SPAs, servers, scripts |
| Request rate | 1-5 req/sec per user | 10-100+ req/sec per client |
| State | Sessions, cookies | Stateless (auth per request) |
| Caching | Browser cache, CDN | ETags, Cache-Control headers |
| Auth mechanism | Login forms, sessions | API keys, JWT, OAuth2 |
| Cross-origin | Same-origin (usually) | CORS required for browser clients |
| Performance metric | Page load time | p50/p99 response latency |
Prerequisites
This guide assumes you have:
- An Ubuntu 24.04 VPS (deploy your API on a Cloud VPS with 2 vCPU / 4GB RAM — APIs are usually CPU-bound, and you can scale CPU independently)
- Nginx installed as a reverse proxy (see our Nginx reverse proxy guide)
- A domain name with DNS pointed to your VPS IP
- Basic firewall configured (UFW setup guide)
Deployment Patterns Overview
Your API needs a process manager to keep it running, restart it on crash, and manage logs. The choice depends on your language:
| Runtime | Process Manager | Guide |
|---|---|---|
| Node.js | PM2 | Deploy Node.js with PM2 |
| Python | Gunicorn + systemd | Deploy Python with Gunicorn |
| Go | systemd (binary) | Covered below |
For a Go API, create a systemd service directly:
# Build the Go binary
cd /home/deploy/api
go build -o api-server ./cmd/server
# Create systemd service
sudo tee /etc/systemd/system/api.service > /dev/null << 'EOF'
[Unit]
Description=REST API Server
After=network.target postgresql.service
[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/home/deploy/api
ExecStart=/home/deploy/api/api-server
Restart=always
RestartSec=5
Environment=PORT=3000
Environment=DATABASE_URL=postgres://appuser:password@localhost:5432/myapp
Environment=GIN_MODE=release
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now api
Nginx as API Reverse Proxy
API reverse proxy configuration differs from website configuration. You need optimized timeouts, proper header forwarding, and JSON-aware error handling.
# /etc/nginx/sites-available/api.conf
upstream api_backend {
server 127.0.0.1:3000;
keepalive 32; # Persistent connections to backend
}
server {
listen 80;
server_name api.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.yourdomain.com;
# SSL configuration (see Let's Encrypt guide)
ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# API-specific: Disable buffering for streaming responses
proxy_buffering off;
# API-specific: Increase request body size for file uploads
client_max_body_size 50M;
# API-specific: Timeouts tuned for API workloads
proxy_connect_timeout 10s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
# Return JSON error responses instead of HTML
error_page 502 503 504 /api_error_502.json;
location = /api_error_502.json {
internal;
default_type application/json;
return 502 '{"error": "Service temporarily unavailable", "status": 502}';
}
error_page 429 /api_error_429.json;
location = /api_error_429.json {
internal;
default_type application/json;
return 429 '{"error": "Rate limit exceeded", "status": 429}';
}
location / {
proxy_pass http://api_backend;
proxy_http_version 1.1;
# Essential proxy headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Request ID for tracing
proxy_set_header X-Request-ID $request_id;
# Keepalive to backend
proxy_set_header Connection "";
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
}
# Enable the site and test
sudo ln -s /etc/nginx/sites-available/api.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
For SSL/TLS certificate setup, follow our Let's Encrypt SSL guide.
Rate Limiting
APIs need rate limiting at both the Nginx level (to protect the server) and the application level (for per-user/per-key limits).
Nginx-Level Rate Limiting
# Add to the http block in /etc/nginx/nginx.conf (or a conf.d file)
# Define rate limit zones
limit_req_zone $binary_remote_addr zone=api_general:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=api_auth:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=api_heavy:10m rate=2r/s;
# Apply in your server block
location /api/v1/ {
limit_req zone=api_general burst=20 nodelay;
limit_req_status 429;
proxy_pass http://api_backend;
# ... other proxy settings
}
# Stricter limits on authentication endpoints
location /api/v1/auth/ {
limit_req zone=api_auth burst=5 nodelay;
limit_req_status 429;
proxy_pass http://api_backend;
}
# Very strict limits on expensive operations
location /api/v1/reports/ {
limit_req zone=api_heavy burst=3 nodelay;
limit_req_status 429;
proxy_pass http://api_backend;
}
The rate=30r/s means 30 requests per second per IP. The burst=20 allows short bursts above the rate before throttling. nodelay means excess requests in the burst are processed immediately rather than queued.
Application-Level Rate Limiting
For per-API-key rate limiting, implement it in your application using Redis as the counter store:
// Node.js example with express-rate-limit and Redis
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');
const redis = new Redis({
host: '127.0.0.1',
port: 6379
});
// Rate limit by API key
const apiKeyLimiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redis.call(...args),
}),
windowMs: 60 * 1000, // 1-minute window
max: 100, // 100 requests per minute per key
keyGenerator: (req) => {
return req.headers['x-api-key'] || req.ip;
},
handler: (req, res) => {
res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: Math.ceil(req.rateLimit.resetTime / 1000),
limit: req.rateLimit.limit,
remaining: req.rateLimit.remaining
});
},
standardHeaders: true, // Return RateLimit-* headers
legacyHeaders: false
});
app.use('/api/', apiKeyLimiter);
CORS Configuration
If your API is consumed by browser-based clients (SPAs, web apps), you need CORS headers. Configure them in Nginx for consistency across all endpoints:
# CORS configuration in Nginx
# Map to handle preflight and actual requests
map $request_method $cors_method {
OPTIONS 11;
default 0;
}
server {
# ... SSL and other config ...
location /api/ {
# CORS headers for all responses
set $cors_origin "";
# Allow specific origins (not wildcard — more secure)
if ($http_origin ~* "^https://(www\.yourdomain\.com|app\.yourdomain\.com)$") {
set $cors_origin $http_origin;
}
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-API-Key, X-Request-ID" always;
add_header Access-Control-Max-Age 86400 always;
add_header Access-Control-Allow-Credentials true always;
# Handle preflight requests
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://api_backend;
# ... other proxy settings
}
}
For a public API that any origin can access:
# Public API CORS (simpler)
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
if ($request_method = OPTIONS) {
return 204;
}
Important: Never use
Access-Control-Allow-Origin: *withAccess-Control-Allow-Credentials: true. Browsers reject this combination. If you need credentials (cookies, auth headers), you must specify exact origins.
Authentication Patterns
API Key Authentication
The simplest authentication method. Validate API keys at the Nginx level for efficiency, or at the application level for flexibility.
# Nginx: Block requests without an API key before they reach your app
location /api/ {
# Require X-API-Key header
if ($http_x_api_key = "") {
return 401 '{"error": "API key required", "status": 401}';
}
proxy_pass http://api_backend;
proxy_set_header X-API-Key $http_x_api_key;
}
For application-level API key validation (recommended for most cases):
// Node.js middleware
const authenticateApiKey = async (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({
error: 'Authentication required',
message: 'Include your API key in the X-API-Key header'
});
}
// Hash the key and look it up (never store plain-text keys)
const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex');
const keyRecord = await db.query(
'SELECT id, user_id, permissions, rate_limit FROM api_keys WHERE key_hash = $1 AND revoked_at IS NULL',
[keyHash]
);
if (keyRecord.rows.length === 0) {
return res.status(401).json({ error: 'Invalid API key' });
}
req.apiKey = keyRecord.rows[0];
next();
};
app.use('/api/', authenticateApiKey);
JWT Validation at Nginx Level
For APIs using JWTs, you can validate tokens at the Nginx level using the ngx_http_auth_jwt_module (Nginx Plus) or validate in your application:
// Node.js JWT validation middleware
const jwt = require('jsonwebtoken');
const authenticateJWT = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: 'Authentication required',
message: 'Include a Bearer token in the Authorization header'
});
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'],
issuer: 'api.yourdomain.com'
});
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
};
// Apply to protected routes
app.use('/api/v1/users/', authenticateJWT);
app.use('/api/v1/orders/', authenticateJWT);
// Public routes (no auth required)
app.get('/api/v1/products', listProducts);
app.get('/api/v1/health', healthCheck);
API Logging and Request Tracing
API debugging requires tracing individual requests across your reverse proxy and application. Use request IDs to correlate log entries.
Nginx Access Log for APIs
# Custom log format for API traffic
log_format api_json escape=json
'{'
'"timestamp":"$time_iso8601",'
'"request_id":"$request_id",'
'"remote_addr":"$remote_addr",'
'"method":"$request_method",'
'"uri":"$request_uri",'
'"status":$status,'
'"body_bytes_sent":$body_bytes_sent,'
'"request_time":$request_time,'
'"upstream_response_time":"$upstream_response_time",'
'"http_user_agent":"$http_user_agent",'
'"http_x_api_key":"$http_x_api_key"'
'}';
server {
access_log /var/log/nginx/api_access.json api_json;
# ...
}
Application-Level Structured Logging
// Node.js structured logging middleware
const requestLogger = (req, res, next) => {
const startTime = process.hrtime.bigint();
const requestId = req.headers['x-request-id'] || crypto.randomUUID();
// Attach request ID to response
res.setHeader('X-Request-ID', requestId);
req.requestId = requestId;
// Log when response finishes
res.on('finish', () => {
const duration = Number(process.hrtime.bigint() - startTime) / 1e6; // ms
const logEntry = {
timestamp: new Date().toISOString(),
requestId,
method: req.method,
path: req.path,
status: res.statusCode,
duration: Math.round(duration * 100) / 100,
userAgent: req.headers['user-agent'],
apiKey: req.apiKey?.id || null,
ip: req.ip
};
if (res.statusCode >= 500) {
console.error(JSON.stringify(logEntry));
} else if (res.statusCode >= 400) {
console.warn(JSON.stringify(logEntry));
} else {
console.log(JSON.stringify(logEntry));
}
});
next();
};
app.use(requestLogger);
For parsing and analyzing logs, see our server logs troubleshooting guide.
Querying Logs by Request ID
# Find a specific request across nginx and application logs
REQUEST_ID="abc123-def456"
# Search Nginx logs
grep "$REQUEST_ID" /var/log/nginx/api_access.json | jq .
# Search application logs (if using PM2)
pm2 logs api --lines 10000 | grep "$REQUEST_ID"
# Search application logs (if using systemd)
journalctl -u api --since "1 hour ago" | grep "$REQUEST_ID"
Health Check Endpoints
Every API should expose health check endpoints. These are used by load balancers, monitoring systems, and deployment tools to determine whether the API is ready to serve traffic.
// Three-tier health check pattern
// Liveness: Is the process running?
app.get('/health/live', (req, res) => {
res.status(200).json({ status: 'alive' });
});
// Readiness: Can the API handle requests?
app.get('/health/ready', async (req, res) => {
try {
// Check database connectivity
await db.query('SELECT 1');
// Check Redis connectivity
await redis.ping();
res.status(200).json({
status: 'ready',
checks: {
database: 'connected',
cache: 'connected'
}
});
} catch (err) {
res.status(503).json({
status: 'not ready',
checks: {
database: err.message.includes('database') ? 'disconnected' : 'connected',
cache: err.message.includes('redis') ? 'disconnected' : 'connected'
}
});
}
});
// Startup: Has the API finished initializing?
let isStarted = false;
app.get('/health/startup', (req, res) => {
if (isStarted) {
res.status(200).json({ status: 'started' });
} else {
res.status(503).json({ status: 'starting' });
}
});
// Mark as started after initialization completes
async function startServer() {
await db.connect();
await redis.connect();
await loadConfiguration();
app.listen(3000, () => {
isStarted = true;
console.log('API server ready on port 3000');
});
}
startServer();
Configure Nginx to use the health check for upstream monitoring:
# Don't log health check requests (they're noisy)
location /health/ {
access_log off;
proxy_pass http://api_backend;
}
API Monitoring with Uptime Kuma
For API monitoring, you need to track more than just "is it up." Response time degradation is an early warning sign that something is wrong, often before outright failure occurs.
Set up Uptime Kuma with API-specific monitors:
# Monitor 1: Liveness (basic uptime)
Type: HTTP(s)
URL: https://api.yourdomain.com/health/live
Interval: 30 seconds
Expected status: 200
# Monitor 2: Readiness (dependency health)
Type: HTTP(s)
URL: https://api.yourdomain.com/health/ready
Interval: 60 seconds
Expected status: 200
Alert if: status is not 200
# Monitor 3: Response time monitoring
Type: HTTP(s)
URL: https://api.yourdomain.com/api/v1/products?limit=1
Interval: 60 seconds
Expected status: 200
Headers: X-API-Key: your-monitoring-api-key
Alert if: response time > 500ms
Set up alerts for response time degradation, not just downtime. A 200 OK response that takes 2 seconds is worse than a 503 that triggers an immediate alert — because the slow response silently degrades user experience while the 503 is obvious.
Capacity Planning: Requests Per Second by VPS Configuration
API throughput depends on your application's complexity, database query patterns, and response payload size. Here are realistic benchmarks for a typical CRUD REST API (JSON responses, database-backed):
| VPS Configuration | Simple Reads (cached) | DB Reads | DB Writes | Suitable For |
|---|---|---|---|---|
| 1 vCPU / 2 GB RAM | 500-1,000 req/s | 100-300 req/s | 50-150 req/s | Internal APIs, MVPs |
| 2 vCPU / 4 GB RAM | 1,000-2,000 req/s | 300-700 req/s | 150-400 req/s | Production APIs, mobile backends |
| 4 vCPU / 8 GB RAM | 2,000-5,000 req/s | 700-1,500 req/s | 400-800 req/s | High-traffic APIs |
| 8 vCPU / 16 GB RAM | 5,000-10,000 req/s | 1,500-3,000 req/s | 800-1,500 req/s | Heavy production workloads |
Benchmark your specific API to get accurate numbers:
# Install wrk (HTTP benchmarking tool)
sudo apt install -y wrk
# Benchmark a read endpoint
wrk -t4 -c100 -d30s -H "X-API-Key: test-key" \
https://api.yourdomain.com/api/v1/products
# Output:
# Running 30s test @ https://api.yourdomain.com/api/v1/products
# 4 threads and 100 connections
# Thread Stats Avg Stdev Max +/- Stdev
# Latency 12.34ms 5.67ms 89.12ms 78.90%
# Req/Sec 812.45 123.67 1.23k 72.34%
# Latency Distribution
# 50% 11.23ms ← p50
# 75% 14.56ms
# 90% 18.90ms
# 99% 34.56ms ← p99 (this is the number that matters)
# 97234 requests in 30.01s, 45.67MB read
# Requests/sec: 3240.78
# Benchmark a write endpoint
wrk -t4 -c50 -d30s -s post.lua \
https://api.yourdomain.com/api/v1/orders
Create the wrk script for POST requests:
-- post.lua
wrk.method = "POST"
wrk.headers["Content-Type"] = "application/json"
wrk.headers["X-API-Key"] = "test-key"
wrk.body = '{"product_id": 1, "quantity": 1}'
A 200ms API response time means your mobile app feels sluggish. For consistent p99 latency under load, MassiveGRID Cloud VDS delivers dedicated CPU that eliminates noisy-neighbor effects — starting at $19.80/mo.
API Versioning in Nginx
When your API evolves, version it properly. Nginx can route different API versions to different application instances:
# Route API versions to different backends
upstream api_v1 {
server 127.0.0.1:3001;
keepalive 16;
}
upstream api_v2 {
server 127.0.0.1:3002;
keepalive 16;
}
server {
# ... SSL config ...
location /api/v1/ {
proxy_pass http://api_v1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/v2/ {
proxy_pass http://api_v2;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Redirect unversioned requests to latest version
location /api/ {
return 301 /api/v2$request_uri;
}
}
Response Compression
JSON compresses extremely well. Enable gzip for API responses:
# /etc/nginx/conf.d/gzip.conf
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 4; # Balance between CPU and compression
gzip_min_length 256; # Don't compress tiny responses
gzip_types
application/json
application/javascript
text/plain
text/css
application/xml;
Verify compression is working:
# Check if response is compressed
curl -s -H "Accept-Encoding: gzip" -H "X-API-Key: test-key" \
-o /dev/null -w "Size: %{size_download} bytes\n" \
https://api.yourdomain.com/api/v1/products
# Compare without compression
curl -s -H "X-API-Key: test-key" \
-o /dev/null -w "Size: %{size_download} bytes\n" \
https://api.yourdomain.com/api/v1/products
Request Validation and Error Handling
A well-designed API returns consistent, predictable error responses. Standardize your error format:
// Standardized error response format
const errorHandler = (err, req, res, next) => {
// Log the full error internally
console.error(JSON.stringify({
timestamp: new Date().toISOString(),
requestId: req.requestId,
error: err.message,
stack: err.stack,
path: req.path,
method: req.method
}));
// Return sanitized error to client
const status = err.status || 500;
const response = {
error: {
code: err.code || 'INTERNAL_ERROR',
message: status === 500
? 'An internal error occurred'
: err.message,
requestId: req.requestId
}
};
// Include validation details for 400 errors
if (status === 400 && err.details) {
response.error.details = err.details;
}
res.status(status).json(response);
};
// Input validation middleware
const validateBody = (schema) => (req, res, next) => {
const errors = [];
for (const [field, rules] of Object.entries(schema)) {
const value = req.body[field];
if (rules.required && (value === undefined || value === null)) {
errors.push({ field, message: `${field} is required` });
continue;
}
if (value !== undefined) {
if (rules.type && typeof value !== rules.type) {
errors.push({ field, message: `${field} must be a ${rules.type}` });
}
if (rules.min !== undefined && value < rules.min) {
errors.push({ field, message: `${field} must be at least ${rules.min}` });
}
if (rules.maxLength !== undefined && value.length > rules.maxLength) {
errors.push({ field, message: `${field} must be at most ${rules.maxLength} characters` });
}
}
}
if (errors.length > 0) {
const err = new Error('Validation failed');
err.status = 400;
err.code = 'VALIDATION_ERROR';
err.details = errors;
return next(err);
}
next();
};
// Usage
app.post('/api/v1/orders', validateBody({
product_id: { required: true, type: 'number', min: 1 },
quantity: { required: true, type: 'number', min: 1 },
notes: { type: 'string', maxLength: 500 }
}), createOrder);
app.use(errorHandler);
API Security Checklist
Before making your API publicly accessible, verify every item on this checklist:
# 1. HTTPS only — no HTTP access
curl -I http://api.yourdomain.com
# Should return 301 redirect to HTTPS
# 2. Security headers present
curl -I https://api.yourdomain.com/api/v1/health/live
# X-Content-Type-Options: nosniff
# X-Frame-Options: DENY
# Strict-Transport-Security: max-age=31536000
# 3. Rate limiting active
for i in $(seq 1 50); do
curl -s -o /dev/null -w "%{http_code}\n" \
https://api.yourdomain.com/api/v1/products
done
# Should see 429 responses after hitting the limit
# 4. Error responses don't leak internals
curl -s https://api.yourdomain.com/api/v1/nonexistent
# Should return JSON error, not stack trace or server info
# 5. Database is not directly accessible
nmap -p 5432 your-vps-ip
# Should show: filtered or closed
# 6. Sensitive endpoints require authentication
curl -s https://api.yourdomain.com/api/v1/users
# Should return 401, not data
For comprehensive server-level security, follow our Ubuntu VPS security hardening guide.
Deployment Workflow for API Updates
A zero-downtime deployment process for your API:
#!/bin/bash
# deploy-api.sh — Zero-downtime API deployment
set -euo pipefail
APP_DIR="/home/deploy/api"
BACKUP_DIR="/home/deploy/api-backup"
echo "=== Starting API deployment ==="
# 1. Pull latest code
cd "$APP_DIR"
git fetch origin main
git reset --hard origin/main
# 2. Install dependencies
npm ci --production
# 3. Run database migrations
npm run db:migrate
# 4. Restart with zero downtime (PM2)
pm2 reload api --update-env
# 5. Wait for the new process to be ready
echo "Waiting for health check..."
for i in $(seq 1 30); do
if curl -sf http://localhost:3000/health/ready > /dev/null 2>&1; then
echo "API is ready after ${i}s"
break
fi
sleep 1
done
# 6. Verify
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/health/ready)
if [ "$STATUS" = "200" ]; then
echo "=== Deployment successful ==="
else
echo "=== Deployment FAILED — rolling back ==="
cd "$APP_DIR"
git reset --hard HEAD~1
npm ci --production
pm2 reload api --update-env
exit 1
fi
Prefer managed API infrastructure? MassiveGRID Managed Dedicated Cloud Servers handle server management, security hardening, SSL renewals, and performance optimization — you focus on building your API.
Summary
Hosting a REST API on a VPS gives you complete control over performance, security, and cost. The critical components are:
- Process manager (PM2, Gunicorn, systemd) keeps your API running
- Nginx reverse proxy handles SSL, rate limiting, CORS, and JSON error responses
- Rate limiting at both Nginx and application levels protects against abuse
- Authentication (API keys or JWT) controls access
- Structured logging with request IDs enables request tracing
- Health checks enable automated monitoring and deployment verification
- Monitoring tracks response time, not just uptime
Start with a 2 vCPU / 4 GB RAM VPS, benchmark your actual throughput, and scale CPU independently as your traffic grows. Most APIs are CPU-bound — adding more vCPU cores scales linearly until you hit database or I/O bottlenecks.