How to Build and Secure a REST API on a VPS

Your framework handles routes and business logic; the server around it decides whether the API is safe to put on the internet. This guide covers the infrastructure side of running a REST API on your own VPS — process management, the reverse proxy, TLS, authentication, input handling, rate limiting and logging — with Node.js examples that translate directly to Python, PHP or Go.

The Shape of a Production API

Client → Firewall → Nginx (TLS, rate limits, headers) → App on 127.0.0.1 → Database on 127.0.0.1

Only Nginx faces the internet. The application and the database listen on the loopback address, so nothing can reach them except through the proxy.

Step 1 — Lock Down the Server First

sudo ufw default deny incoming
sudo ufw allow OpenSSH
sudo ufw allow 80,443/tcp
sudo ufw enable

Use SSH keys and turn off password login before the API goes live — see How to Harden a Fresh Linux VPS.

Step 2 — Bind the App to Localhost

app.listen(3000, '127.0.0.1');

Never expose the application port directly. If ss -tulpn shows it on 0.0.0.0, it is reachable from anywhere the firewall allows.

Step 3 — Keep It Running with a Process Manager

Run the app under systemd (or PM2) so it restarts on crash and on reboot, and runs as an unprivileged user rather than root:

[Unit]
Description=My API
After=network.target

[Service]
User=api
WorkingDirectory=/srv/api
EnvironmentFile=/srv/api/.env
ExecStart=/usr/bin/node server.js
Restart=on-failure
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now api

Which to choose is covered in PM2 vs systemd vs Supervisor.

Step 4 — Keep Secrets Out of the Code

Database passwords, JWT signing keys and API tokens belong in an environment file readable only by the service user — never in the repository:

sudo chown api:api /srv/api/.env
sudo chmod 600 /srv/api/.env

Step 5 — Put Nginx in Front, with TLS

server {
    server_name api.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        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;
        client_max_body_size 1m;
    }
}
sudo certbot --nginx -d api.example.com

Certbot adds HTTPS and the redirect from HTTP. Never accept credentials or tokens over plain HTTP. Full proxy setup: Nginx as a Reverse Proxy for Node.js and Docker Apps. If the API has no domain yet, see IP-only TLS options.

Behind a proxy, tell the framework to trust it so client IPs in logs and rate limits are real: in Express, app.set('trust proxy', 'loopback').

Step 6 — Authenticate and Authorise

Authentication proves who the caller is; authorisation decides what they may touch. Check both on every request, and check ownership on every object (“can this user read invoice 123?”), not just whether they are logged in. Token-based authentication is covered in How to Set Up API Authentication with JWT.

Step 7 — Validate Every Input on the Server

const { body, validationResult } = require('express-validator');

app.post('/users',
  body('email').isEmail().normalizeEmail(),
  body('age').isInt({ min: 0, max: 150 }),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
    // ... handle valid input
  });

Client-side validation is for user experience; only server-side validation is security.

Step 8 — Use Parameterised Queries

// Vulnerable to SQL injection
db.query(`SELECT * FROM users WHERE email = '${email}'`);

// Safe
db.query('SELECT * FROM users WHERE email = ?', [email]);

Step 9 — Rate Limit

# in the http {} block
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

# in the location {} block
limit_req zone=api burst=20 nodelay;

Stricter limits belong on login and password-reset endpoints. Details in How to Rate Limit an API with Nginx.

Step 10 — Set CORS and Security Headers Deliberately

app.use(cors({ origin: 'https://app.example.com', methods: ['GET', 'POST', 'PUT', 'DELETE'] }));
app.use(helmet());

Avoid origin: '*' on any API that uses cookies or returns private data.

Step 11 — Return Errors Without Leaking Internals

app.use((err, req, res, next) => {
  console.error(err);                                   // full detail to your logs
  res.status(500).json({ error: 'Internal server error' }); // nothing internal to the client
});

Step 12 — Log, Monitor, Version

  • Request logs with status and latency (morgan, or Nginx access logs), rotated so they do not fill the disk.
  • A /health endpoint for uptime checks — see How to Set Up Uptime Monitoring.
  • Versioned routes (/v1/) so you can change the API without breaking clients.

Production Checklist

  • Firewall on; only 22, 80 and 443 open
  • App and database on 127.0.0.1, app running as a non-root user
  • HTTPS everywhere, certificates renewing
  • Secrets in a 600-permission env file, not in Git
  • Authentication and per-object authorisation on every route
  • Server-side validation and parameterised queries
  • Rate limits, tighter on auth endpoints
  • No stack traces in responses; logs rotated; health check monitored

Choosing a Server for It

Most APIs are database-bound long before they are CPU-bound, so fast storage matters more than core count — NVMe VPS is the usual fit. Every plan includes full root access and a dedicated IPv4; VPS for developers covers what to pick for staging versus production.

Common Problems

502 Bad Gateway — the app is not running or not on the port Nginx proxies to: systemctl status api and ss -tulpn | grep 3000. See How to Fix Common Nginx Errors.

Every request appears to come from 127.0.0.1 — the framework is not trusting the proxy headers; set trust proxy.

CORS errors in the browser — the origin must match the front end’s scheme, host and port exactly.

Continue Reading

Browse more articles in Object Storage, Messaging & APIs.

  • rest api, api security, api deployment, backend security
  • 35 Users Found This Useful
Was this answer helpful?

Related Articles

How to Set Up Self-Hosted S3-Compatible Object Storage with MinIO

MinIO is a high-performance, self-hosted object storage server compatible with the S3 API —...

How to Use Object Storage for Application File Uploads

Storing user-uploaded files directly on your application server's disk creates scaling and...

How to Install and Configure RabbitMQ on a VPS

RabbitMQ is a widely-used, robust message broker — enabling applications to communicate...

How to Install and Configure Redis as a Message Queue

Redis, primarily known as a cache, also works well as a lightweight message queue for simpler use...

How to Design and Secure Webhook Endpoints

Webhooks let external services notify your application of events in real time. Since they accept...