Managing Connections in Express Apps
June 2021
Typically when building apps in Express you’ll be using some sort of persistence mechanism, whether that is a relational database, NoSQL database, or another object store. Certain circumstances require using two stores simultaneously, perhaps if you want to use an RDBMS as a source of truth but leverage something like Redis as a cache to boost overall performance. Whatever technology you’re using in your application, you’ll want to ensure that connections are established (or have failed fast) before you fully boot your app.
Introductory tutorials often take the approach of initializing the database connection independently from booting the Express app – this tutorial is a top result but there are many like it. The typical approach is as follows (we’re using Sequelize with sqlite here):
const app = express();
app.use(express.json());
sequelize
.authenticate()
.then(() => {
console.log("Database connection established");
})
.catch((err) => {
console.log(`Connection failed: ${err}`);
});
app.get("/users", async (_, resp) => {
const users = await User.findAll(); // User is a Sequelize model
resp.status(200).json({ users });
});
app.listen(3000, () => {
console.log("Listening on port 3000");
});
The problem may not be obvious, but we’ve created a situation where 2 unintended outcomes are possible:
- Requests to the
/users
endpoint return no data because a request is made before the database connection is established - The connection to the database fails, but the application starts in a broken
state (requests to
/users
hang since the database is unavailable)
We can handle the second scenario by exiting the process when the database connection fails, but there could be a period of time where the app starts handling requests waiting for the connection to finally time out.
Fortunately, there are a couple of solutions to ensure that the application starts in a valid state.
Wait for the Connection Promise to Resolve
What might be the quickest solution is to move the “listen” operation inside of the resolved promise after the connection is established:
app.get("/users", async (_, resp) => {
const users = await User.findAll(); // User is a Sequelize model
resp.status(200).json({ users });
});
sequelize
.authenticate()
.then(() => {
console.log("Database connection established");
app.listen(3000, () => {
console.log("Listening on port 3000");
});
})
.catch((err) => {
console.error(`Connection failed: ${err}`);
process.exit(1);
});
This way, the application will only be available once the database connection is
available, and the process.exit
call will ensure that the process exits with
an error status for anything monitoring it. This can also be accomplished using
async
/ await
:
(async () => {
try {
await sequelize.authenticate();
console.log("Database connection established");
} catch (e) {
console.error(e);
process.exit(1);
}
app.listen(3000, () => {
console.log("Listening on port 3000");
});
})();
Use Node’s Built-In Event Handling
Node supports an event-driven architecture at its core, and Express builds on these core capabilities. Inspired by this blog post, we can adopt an approach that listens for events before starting the server:
app.get("/users", async (_, resp) => {
const users = await User.findAll(); // User is a Sequelize model
resp.status(200).json({ users });
});
app.once("connected", () => {
app.listen(3000, () => {
console.log("Listening on port 3000");
});
});
sequelize
.authenticate()
.then(() => {
console.log("Database connection established");
app.emit("connected");
})
.catch((e) => {
console.error(e);
process.exit(1);
});
Similar to the previous example, the app will only boot once the event has been
emitted. Though this could result in some disconnect between the strings used in
the event handlers, that can be easily mitigated using a TypeScript enum
:
enum Events {
Connected = "connected",
}
app.once(Events.Connected, () => {
app.listen(3000, () => {
console.log("Listening on port 3000");
});
});
sequelize
.authenticate()
.then(() => {
console.log("Database connection established");
app.emit(Events.Connected);
})
.catch((e) => {
console.error(e);
process.exit(1);
});
Again, it’s possible to use async
/ await
semantics:
(async () => {
try {
await sequelize.authenticate();
console.log("Database connection established");
} catch (e) {
console.error(e);
process.exit(1);
}
app.emit(Events.Connected);
})();
Both of these approaches provide a cleaner way to ensure that your Express applications boot successfully or fail fast.
This article was originally posted on the KindHealth engineering blog.