Appearance
Express Utility Handlers
Reusable Express middleware for common patterns: blocking requests until an async operation completes, and sending standard 404 responses.
When to Use
- Delaying HTTP serving until a build, migration, or other startup task finishes
- Adding a standard 404 handler at the end of the middleware chain
- Gating routes behind async preconditions without custom middleware per route
The Pattern
Wait for database migration
Gate the entire server behind an async precondition using ExpressHandlers.waitUntilResolved():
javascript
import express from "express";
import { ExpressHandlers } from "./http.js";
import { runMigrations } from "./db.js";
const app = express();
const ready = runMigrations();
app.use(ExpressHandlers.waitUntilResolved(ready));
app.get("/api/users", listUsers);
app.listen(3000);Requests to /api/users will block until migrations complete, then proceed normally.
Final 404 handler
Place ExpressHandlers.notFound at the end of the middleware chain to send standard 404 responses for unmatched requests:
javascript
app.use("/api", apiRouter);
app.use(uiServer);
app.use(ExpressHandlers.notFound);Any request that falls through all previous middleware gets a 404 Cannot GET /path response.
Implementing ExpressHandlers
The ExpressHandlers class provides two static methods — one for gating requests behind an async operation, another for standard 404 responses:
javascript
class ExpressHandlers {
/**
* Blocks all requests until the given promise
* resolves, then passes through to the next
* middleware.
*/
static waitUntilResolved(promise) {
return async function (req, res, next) {
await promise;
next();
};
}
/**
* Sends a 404 response matching Express's
* default format.
*/
static notFound(req, res) {
res.status(404).send(
`Cannot ${req.method} ${req.originalUrl}`,
);
}
}How waitUntilResolved works
It returns a middleware function that awaits the promise on every request. Once the promise settles, next() is called and the request proceeds. While the promise is pending, all requests queue behind it.
The promise is shared. Multiple requests don't each trigger the async operation — they all wait on the same promise instance.
Inline variant
The same pattern appears as a local function when the promise is created nearby. This version gates requests behind a dev build:
javascript
app.use(awaitDevBuild());
app.use(serveDistDir());
function awaitDevBuild() {
const buildComplete = devBuild();
return async function (req, res, next) {
await buildComplete;
next();
};
}Trade-offs
| Approach | Pros | Cons |
|---|---|---|
waitUntilResolved | Simple, shared promise | Blocks all routes |
| Per-route async guard | Fine-grained control | Repetitive |
| Health check endpoint | Doesn't block | Client must poll |
Use waitUntilResolved for all-or-nothing startup gates. If the server shouldn't serve anything until a precondition is met (build complete, DB ready), this is the simplest approach. For partial availability, use per-route guards instead.