Key Takeaways
- Express.js is a persistent framework that expects an always-on connection pool, which makes it a poor fit for serverless hosting, especially with a database like PostgreSQL.
- Bare-metal VPS hosting requires manually configuring Nginx, PM2, and SSL, while serverless platforms spin up fresh instances on every request and can overwhelm Postgres connection limits.
- A managed, persistent Node.js environment like Cloudways Velocity keeps the Express process warm and the database connection pool open without manual server administration.
- This guide walks through building a local Express and PostgreSQL Task Manager API, pushing it to GitHub, and deploying it live on Cloudways Velocity.
Localhost is easy. It always is. You connect your database, map out a few REST endpoints, run your Node script, and everything feels incredibly fast.
Then you actually try to put that Express backend on the internet. Suddenly, you have to make a choice about infrastructure, and standard options usually leave you frustrated.
You either rent a blank Linux server and spend your weekend acting as an unpaid sysadmin, or you dump the codebase into a serverless edge platform that puts your API to sleep the second people stop using it.
If you are hooking up a relational database like PostgreSQL, ephemeral serverless hosting is going to give you a massive headache. You need a persistent backend.
I am going to walk through building a fast Task Manager API with Express and deploying it straight to a persistent Cloudways Managed Node.js (Velocity) server. No Nginx configurations to debug. No serverless cold starts. Just reliable express js hosting.
Bare-Metal VPS vs. Serverless Hosting for Express
Let’s look at the two extreme ends of Node hosting and why neither is perfect for a standard API.
On one side, you have the raw bare-metal VPS. You get an IP address, root access, and zero help. Getting an Express app running here means installing Node manually. You have to fight with Nginx reverse proxy blocks just to expose port 3000 to the web. You must set up PM2 to manage the background processes, and you are entirely responsible for your own SSL certificates. It is a massive time sink.
Because of this, everyone eventually pivots to serverless platforms. You just upload the repository, and the cloud provider runs it.
But here is the catch. Express is fundamentally a persistent framework. It wants to stay awake. It wants to hold a database connection pool open in RAM so it doesn’t have to constantly handshake with your database.
Serverless platforms kill your application when web traffic drops. When a new user hits the endpoint, the cloud spins up a fresh instance from scratch. That new instance has to open a brand-new connection to your Postgres database.
Postgres has a hard limit on concurrent connections. If your API gets 150 hits at once, a serverless platform will spin up 150 separate instances. They all try to connect to the database simultaneously. Postgres panics, hits its connection limit, and your backend crashes entirely.
Why a Persistent Node.js Server is the Best Fit
This database connection bottleneck is exactly why you need a persistent Node environment.
A dedicated server never puts your Express process to sleep. It stays warm. Because the process never shuts down, it can hold a single, highly efficient database connection pool open indefinitely. When a query comes in, there is no startup delay. The data moves instantly.
Using a managed environment like Cloudways Velocity gives you that persistent power without the DevOps nightmare. The platform handles the proxy routing, process management, and security patches automatically. You just push code to your repository, and the server keeps the lights on.
Get a Persistent Node.js Environment in Minutes
Cloudways Velocity keeps your Express process warm so your database connections never reset.
Mini Project: Build a Task Manager API
I’m going to build a minimal Task Manager API. I’ll write a custom Express backend that handles basic CRUD operations and connects to a PostgreSQL database.
Setting Up the Local Project
The first thing I’ll do is set up my local workspace.
I’m using my work laptop for this demo which has IT restrictions so I can’t run the standard installer for Node. I’ll instead download the standalone binary .zip version of Node.js on my machine.
After unzipping the Node folder, I’ll tell Command Prompt where it is located on my machine. To do this, I’ll run this command with the exact path:
set PATH=%PATH%;C:\Users\abdulrehman\Downloads\node-v24.18.0-win-x64\node-v24.18.0-win-x64
Great, now I can execute Node and npm commands.
Next I’ll create a project folder on my desktop.
cd C:\Users\abdulrehman\Desktop mkdir cloudways-express-api

cd cloudways-express-api npm init -y

Next up, I will install the dependencies. I need Express for routing. I need CORS for cross-origin requests. I also need dotenv for local environment variables and pg to connect with PostgreSQL.
npm install express cors dotenv pg

I also need a local DB to test the mini project. But again, since I’m using my office laptop, I’ll download and extract the standalone .zip version to my downloads folder.
And just like last time, I’ll tell CMD where the Postgres tools live on my machine:
set PATH=%PATH%;C:\Users\abdulrehman\Downloads\postgresql-18.4-2-windows-x64-binaries\pgsql\bin
![]()
Now, I can initialize the local database cluster. I run this to create a new data folder inside that extracted directory:
initdb.exe -D "C:\Users\abdulrehman\Downloads\postgresql-18.4-2-windows-x64-binaries\pgsql\data" -U postgres -W
After running the command, PostgreSQL asks me to set a superuser password. I’ll save it somewhere as I’ll need it later.

That -W flag at the end is super important. It forces the terminal to pause and ask for a new superuser password. I just typed in a simple password to use for my local testing.
Next, I need to turn the database server on so it runs quietly in the background:
pg_ctl.exe -D "C:\Users\abdulrehman\Downloads\postgresql-18.4-2-windows-x64-binaries\pgsql\data" -l logfile start

The server is running now, but it is totally empty. My Express app needs a specific database to connect to, so I’ll create one named task_api_db:
createdb.exe -U postgres task_api_db

That’s it. My local database is fully operational at this stage.
Writing the Express Server
To test real-world persistence, keeping a PostgreSQL connection pool active is the perfect stress test. I will open up my editor and create a server.js file. Drop in my custom script:
// server.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const { Pool } = require('pg');
const app = express();
const port = process.env.PORT || 3000;
app.use(cors());
app.use(express.json());
// Initialize PostgreSQL Connection Pool
// This relies on the environment variable we will set in Cloudways
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
// If the URL contains 'localhost', turn SSL off. Otherwise, turn it on for Cloudways.
ssl: process.env.DATABASE_URL.includes('localhost') ? false : { rejectUnauthorized: false }
});
// Initialize the database table on startup
const initDB = async () => {
const createTableQuery = `
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
completed BOOLEAN DEFAULT FALSE
);
`;
try {
await pool.query(createTableQuery);
console.log("Database table initialized successfully.");
} catch (err) {
console.error("Error creating table:", err);
}
};
initDB();
// API Route: Get all tasks
app.get('/api/tasks', async (req, res) => {
try {
const result = await pool.query('SELECT * FROM tasks ORDER BY id DESC');
res.json(result.rows);
} catch (err) {
res.status(500).json({ error: 'Failed to fetch tasks' });
}
});
// API Route: Create a new task
app.post('/api/tasks', async (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ error: 'Title is required' });
}
try {
const result = await pool.query(
'INSERT INTO tasks (title) VALUES ($1) RETURNING *',
[title]
);
res.status(201).json(result.rows[0]);
} catch (err) {
res.status(500).json({ error: 'Failed to create task' });
}
});
// Start the persistent server
app.listen(port, () => {
console.log(`Express API listening on port ${port}`);
});

The logic here is incredibly important. Look closely at the PostgreSQL connection setup. By using process.env.DATABASE_URL, I am explicitly telling Express to use an environment variable for the database.
If I were to host this on a standard serverless platform, tearing down and rebuilding this connection pool on every request would instantly bottleneck the database. It strictly requires a live, persistent Node.js process to keep the pool active.
To test what I built locally, I’ll create a new file named .env inside my project folder, paste my local Postgres connection string into it, and run node server.js.
DATABASE_URL=postgres://postgres:your_password_here@localhost:5432/task_api_db


After running the node server.js command, the terminal shows a confirmation that the database table is initialized.
To see the API working, I open my web browser and navigate to http://localhost:3000/api/tasks.
The page loads a blank set of brackets: [].

This is exactly what I expected to see. It means Express successfully queried the Postgres database, and the database correctly responded that the table is empty.
To verify that the database can actively save data, I open a second Command Prompt window and fire off a quick POST request using cURL:
curl -X POST http://localhost:3000/api/tasks -H "Content-Type: application/json" -d "{\"title\":\"Finish the Cloudways deployment!\"}"

When I switch back to my browser and refresh the page, the empty brackets are gone. Instead I can now see {“id”:1,”title”:”Finish the Cloudways deployment!”,”completed”:false}.
This means my local Express server successfully received the POST request, parsed the JSON payload, ran the SQL insertion query, and stored the task inside my local PostgreSQL database.
The mini project is fully operational on my local machine. Now I can push this codebase to Git and deploy it to Cloudways Velocity.
Pushing the Project to GitHub
Before I do the push, I will open my package.json file and manually add a start script. It should look exactly like this:
"scripts": {
"start": "node server.js"
}
My updated code will look like this:
{
"name": "cloudways-express-api",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"start": "node server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"pg": "^8.23.0"
}
}

The start command is critical. Cloudways looks for this exact keyword when configuring the production server.
I’ll go to GitHub and then create a new repository. I’ll call it cw-velocity-express-api.

Then I’ll go back to my command prompt, and push the project to my GitHub repo:
git init git add . git commit -m "Initial commit: Express Task API with PostgreSQL" git branch -M main git remote add origin https://github.com/abdulrehman293/cw-velocity-express-api git push -u origin main

With my code sitting safely in version control, it is time to tackle the infrastructure.
Deploying the Mini Project on Cloudways Velocity
Deploying a custom Node.js application on bare-metal VPS servers usually means configuring Nginx as a reverse proxy, keeping PM2 running, and taking care of SSL certificates.
Cloudways Velocity handles such server-side tasks on its own. It provides a managed Node.js environment where the underlying server setup is handled for me, and it also provisions a PostgreSQL database as part of the setup.
Back to the deployment. I’ll open the Cloudways console and select Velocity, then click Get Started.

For this deployment, I’m going to choose the Starter plan, which is more than enough for this demo.

Next, I connect my GitHub account, select my cw-velocity-express-api repository and click Continue.


Cloudways will now automatically choose everything for me. For example, it checks the root of the project for the package.json file. From there, it reads the dependencies, detects Express, and selects the appropriate Express framework preset automatically.
There’s no need for me to create custom start commands or set up PM2 manually. The platform handles those pieces as part of the deployment.
All I need to do is, add an environment variable to securely inject my database credentials directly into the production Node process.

But to do this, I first need to deploy the app. Once it’s done deploying, I can access database details for my app and construct my connection string to add to the environment variable using this format:
postgres://[Username]:[Password]@localhost:5432/[DB Name]
So I’ll click Deploy Now for now and let the deployment complete.


Now that the app is deployed, I’ll open my application on Cloudways and click on the Database tab.

Since I used PostgreSQL in the local deployment, I’ll connect PostgreSQL to Cloudways.

Once connected, I can now see the DB Name, Username and password for PostgreSQL. I’ll use this to construct my connection string which I talked about earlier.

I’ll take the connection string I created and add it as an environment variable in Cloudways like so:

Now that everything is done, I’ll head back to the application’s Overview tab and copy the Application URL. I’ll open it in the browser and add /api/tasks to the end of the URL to check the API.

An empty JSON array loads instantly, just like it did on local.

To test saving real data, I open Command Prompt and run a POST request to my live Cloudways URL:
curl -X POST https://nodejs-1658614-6605116.cloudwaysnodeapps.com/api/tasks -H "Content-Type: application/json" -d "{\"title\":\"Live test on Cloudways Node.js hosting Velocity!\"}"
![]()
When I refresh the page now, I can see the task I pushed via the POST request.

And to check if it got saved in the PostgreSQL database on Cloudways, I’ll launch the database manager.

Here I can see the tasks table and also the task saved with its ID in the database.

And with that, I am experiencing a true, always-on Express instance. No connection timeouts. No cold starts.
Ready to Deploy Your Own Express API?
Connect your GitHub repo and let Cloudways Velocity handle the server setup for you.
Wrapping Up
That brings this Express.js deployment guide to an end. I covered why a persistent Node.js environment can be a better fit for APIs that need consistent performance, particularly when the application relies on keeping database connections active.
I also built an Express API that connects to PostgreSQL locally, tested the project, pushed it to GitHub, and then deployed the finished application using Cloudways Managed Node.js Hosting (Velocity).
The complete project is available on my GitHub if you’d like to use it as a starting point for your own Express.js application. If you run into any issues with the setup or deployment steps, feel free to leave a question in the comments.
Q. What exactly is ExpressJS used for?
Express.js is a minimal web framework for Node.js. Developers use it to build backend APIs, handle HTTP requests, and manage routing. It acts as the backend engine for single-page applications and complex web services.
Q. Is ExpressJS a server?
No. Express.js is a framework that runs inside the Node.js runtime environment. The framework provides routing and structure. Node.js physically creates and manages the actual HTTP server process.
Q. Why is ExpressJS so popular?
It is incredibly lightweight and unopinionated. It does not force a strict directory structure. It does not mandate a specific ORM. This flexibility allows teams to rapidly prototype backends using any database they choose.
Q. Is ExpressJS RESTful?
Yes. Developers heavily utilize Express.js to construct RESTful APIs. Its core architecture routes standard HTTP methods like GET, POST, PUT, and DELETE to specific URL endpoints. This makes it ideal for designing clean, stateless REST interfaces.
Start Growing with Cloudways Today.
Our Clients Love us because we never compromise on these
Abdul Rehman
Abdul is a tech-savvy, coffee-fueled, and creatively driven marketer who loves keeping up with the latest software updates and tech gadgets. He's also a skilled technical writer who can explain complex concepts simply for a broad audience. Abdul enjoys sharing his knowledge of the Cloud industry through user manuals, documentation, and blog posts.