This guide covers the essential security and architecture practices for deploying a REST API on your own VPS, complementing your framework-specific development work with infrastructure-level best practices.
Basic Architecture
Client → Nginx (reverse proxy, SSL, rate limiting) → Application (API logic) → Database
Step 1 — Bind Your API to Localhost Only
Never expose your application's raw port directly to the internet — run it behind Nginx:
app.listen(3000, '127.0.0.1');
Step 2 — Configure Nginx as a Reverse Proxy
See Nginx as a Reverse Proxy for Node.js/Docker Apps for the full setup, including proper header forwarding.
Step 3 — Always Use HTTPS
sudo certbot --nginx -d api.yourdomain.com
Never accept API credentials or data over plain HTTP.
Step 4 — Implement Authentication
See How to Set Up API Authentication with JWT for token-based authentication, the most common approach for modern APIs.
Step 5 — Validate All Input
Never trust client input — validate types, lengths, and formats server-side, regardless of any client-side validation already performed:
const { body, validationResult } = require('express-validator');
app.post('/users',
body('email').isEmail(),
body('age').isInt({ min: 0, max: 150 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// ... process valid input
}
);
Step 6 — Rate Limit API Endpoints
See How to Rate Limit an API with Nginx to prevent abuse and protect backend resources.
Step 7 — Set Appropriate CORS Policy
app.use(cors({
origin: 'https://yourfrontend.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));
Avoid overly permissive CORS (origin: '*') for APIs handling authenticated/sensitive data.
Step 8 — Return Consistent, Non-Leaky Error Responses
app.use((err, req, res, next) => {
console.error(err); // log the full detail server-side
res.status(500).json({ error: 'Internal server error' }); // don't leak stack traces to clients
});
Detailed error messages/stack traces should go to server-side logs, never directly to API responses in production.
Step 9 — Use Parameterized Queries (Prevent SQL Injection)
// Vulnerable
db.query(`SELECT * FROM users WHERE email = '${email}'`);
// Safe
db.query('SELECT * FROM users WHERE email = ?', [email]);
Step 10 — Log API Requests for Monitoring and Debugging
app.use(morgan('combined'));
Step 11 — Version Your API
See How to Version an API Without Breaking Existing Clients.
Step 12 — Add Health Check Endpoints
See How to Add Health Check Endpoints to Your Application.
Security Checklist for Production APIs
- HTTPS enforced everywhere
- Authentication and authorization properly implemented
- All input validated server-side
- Rate limiting configured
- Parameterized database queries throughout
- No sensitive information leaked in error responses
- CORS configured restrictively
Common Errors
API works locally but 502s in production — verify the app is actually running and bound to the port Nginx is proxying to; see How to Fix Common Nginx Errors (502/504/403).
Related Articles
- How to Set Up API Authentication with JWT
- How to Rate Limit an API with Nginx
- Nginx as a Reverse Proxy for Node.js/Docker Apps
