Support for Express 5, connect-redis 9 & argon2 in Wappler NodeJS Projects

Running ncu -u (npm-check-updates) to upgrade all packages to their latest versions causes breakages in the following Wappler Server Connect core modules:

  • lib/server.js — breaks due to Express 5 + Node 22 async timing changes
  • lib/setup/routes.js — breaks due to Express 5's path-to-regexp v8 syntax changes and async timing
  • lib/oauth/index.js — breaks due to Express 5 session save timing on redirects
  • lib/setup/session.js — breaks due to connect-redis 9 requiring redis (node-redis) client instead of ioredis
  • Dockerfile — breaks due to argon2 requiring native build tools not present in slim Docker images

The primary packages that cause these breakages when upgraded via ncu -u:

Upgraded PackageOld → NewModules Affected
express^4.21.2^5.2.1lib/server.js, lib/setup/routes.js, lib/oauth/index.js
connect-redis^6.x^9.0.0lib/setup/session.js
argon2not included → ^0.44.0Dockerfile (native build failure)

This document details each breaking change and the fix applied.

Package Version Changes

PackageOld Version (Wappler default)New VersionBreaking?Notes
express^4.21.2^5.2.1Yespath-to-regexp v8, async timing changes
connect-redis^6.x^9.0.0YesNamed export { RedisStore }, requires redis (node-redis) client instead of ioredis
redis (node-redis)not included^5.11.0N/ANew dependency — required by connect-redis 9 for session store
argon2not included (used bcryptjs)^0.44.0N/ANew dependency — faster & more secure password hashing, requires native build tools (python3, make, g++) in Docker
Node.js18 / 2022YesNew microtask/Promise scheduling exposes latent async bugs

Breaking Changes & Required Fixes

1. Async Route Registration (lib/server.js)

Problem: The routes() function in lib/setup/routes.js is async but was called without await. In Express 4 with older Node.js, async operations happened to resolve synchronously, so routes were registered before the 404 handler by luck. With Node 22, Promise/microtask scheduling changed, causing the 404 catch-all handler to be registered before routes finished — resulting in 404 for all routes.

Fix:

// Before (Express 4)
upload(app);
secure(app);
routes(app);
auth(app);

module.exports = {
    start: function({ port, httpsOptions } = {}) {
        const server = httpsOptions ? ...

// After (Express 5)
upload(app);
secure(app);

let routesReady = routes(app);
auth(app);

module.exports = {
    start: async function({ port, httpsOptions } = {}) {
        await routesReady;
        const server = httpsOptions ? ...

Root Cause: Latent bug — routes() returns a Promise that was never awaited. Node 22's microtask timing exposed it.


2. Async API Route Creation (lib/setup/routes.js)

Problem: Inside routes.js, createApiRoutes('app/api') uses map() from lib/core/async.js which wraps Promise.all — making it async. Without await, API routes (e.g., /api/admin/login) were not registered by the time the routes() function's Promise resolved.

Fix:

// Before (Express 4)
if (config.createApiRoutes) {
    fs.ensureDirSync('app/api');
    createApiRoutes('app/api');
}

// After (Express 5)
if (config.createApiRoutes) {
    fs.ensureDirSync('app/api');
    await createApiRoutes('app/api');
}

Root Cause: Same as #1 — unresolved Promise, exposed by Node 22 timing changes.


3. Route Path Syntax (lib/setup/routes.js)

Problem: Express 5 upgraded path-to-regexp from v0.1.x to v8.x, which no longer supports regex-style optional groups in route paths. Routes like /api/add_applicant(.json)? threw PathError: Unexpected ( at index 18.

Fix:

// Before (Express 4 — path-to-regexp v0.1.x)
let routePath = path.replace(/^app/i, '').replace(/.json$/, '(.json)?');

// After (Express 5 — path-to-regexp v8.x)
let routePath = path.replace(/^app/i, '').replace(/.json$/, '{.json}');</code></pre>

path-to-regexp v8 syntax changes:

Express 4 (v0.1.x)Express 5 (v8.x)Description
(.json)?{.json}Optional static text
/:param?{/:param}Optional parameter
(.*){*path}Wildcard/catch-all

Reference: https://github.com/pillarjs/path-to-regexp/releases


4. OAuth Session Persistence (lib/oauth/index.js)

Problem: The OAuth flow sets a state value in the session before redirecting to the OAuth provider. In Express 4, express-session reliably saved the session via its res.end() hook before the redirect response was sent. In Express 5 with Node 22, the redirect response could be sent before the session was written to the store, causing the state check to fail on callback — resulting in an infinite redirect loop (ERR_TOO_MANY_REDIRECTS).

Fix:

// Before (Express 4)
this.app.setSession(`${this.name}_state`, params.state);
this.app.res.redirect(build_url(this.opts.auth_endpoint, params));

// After (Express 5)
this.app.setSession(`${this.name}_state`, params.state);

// Explicitly save session before redirect to ensure state persists
await new Promise((resolve, reject) =&gt; {
    this.app.req.session.save((err) =&gt; {
        if (err) return reject(err);
        resolve();
    });
});

this.app.res.redirect(build_url(this.opts.auth_endpoint, params));

Root Cause: res.end() hook timing changed in Express 5 / Node 22 — session auto-save no longer guaranteed before redirect completes.


5. connect-redis 9: Requires redis (node-redis) Client (lib/setup/session.js)

Problem: connect-redis v9 uses the redis (node-redis v4+) client API: client.set(key, value, { EX: ttl }). Wappler's default setup passes an ioredis client (global.redisClient) which doesn't support the object options syntax — it stringifies the options to [object Object], causing Redis to reject the command.

Docker logs showed:

ReplyError: ERR syntax error
  command: {
    name: 'set',
    args: [
      'sess:kEVJPHiC...',
      '{"cookie":...}',
      '[object Object]'    &lt;-- should be 'EX', 900
    ]
  }

Fix:

// Before (connect-redis 6 — uses ioredis via .default export)
if (options.store.$type == 'redis') {
    const RedisStore = require('connect-redis').default;
    options.store = new RedisStore(Object.assign({
        client: global.redisClient   // ioredis client — incompatible with connect-redis v9
    }, options.store));
}

// After (connect-redis 9 — uses node-redis via named export)

if (options.store.$type == 'redis') {
    const { RedisStore } = require('connect-redis');
    const { createClient } = require('redis');
    const redisUrl = config.redis === true ? 'redis://redis' : config.redis;
    const sessionRedisClient = createClient({ url: redisUrl });
    sessionRedisClient.connect().catch(err =&gt; console.error('Session Redis connect error:', err));
    const storeOpts = { ...options.store };
    delete storeOpts.$type;
    options.store = new RedisStore(Object.assign({
        client: sessionRedisClient   // node-redis client — compatible with connect-redis v9
    }, storeOpts));
}

Key differences:

  • Import: require('connect-redis').default (v6) → { RedisStore } = require('connect-redis') (v9)
  • Client: ioredis (global.redisClient) → redis (node-redis v4+) with separate createClient()
  • Note: global.redisClient (ioredis) is still used for other Redis operations (Socket.IO adapter, rate limiting, etc.). Only the session store needs the redis package client.

6. Dockerfile: argon2 Native Build Dependencies

Problem: The argon2 npm package requires native compilation (uses node-gyp with C++ bindings). The Slim Deployment Wappler Dockerfile are based on node:20-bullseye-slim and do not include the necessary build tools, causing npm install to fail with build errors.

Fix — add to Dockerfile before npm install:

RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*

Dockerfile example:

FROM node:22-bullseye-slim

ARG NODE_ENV=production
ENV NODE_ENV $NODE_ENV

ARG PORT=3000
ENV PORT $PORT
EXPOSE $PORT

RUN apt-get update &amp;&amp; apt-get install -y python3 make g++ &amp;&amp; rm -rf /var/lib/apt/lists/*

ENV PATH /opt/node_app/node_modules/.bin:$PATH
ENV NODE_ENV development
WORKDIR /opt/node_app
COPY index.js .
COPY package.json .
RUN npm install --no-optional --no-package-lock
CMD [ "nodemon", "./index.js" ]

Note: This is required for any project using argon2 for password hashing. The build tools (python3, make, g++) are needed by node-gyp to compile the native addon.


Files Changes that are required

FileChangeWhy
lib/server.jsawait routesReady before starting serverNode 22 microtask timing exposes unresolved Promise
lib/setup/routes.jsawait createApiRoutes() + {.json} path syntaxAsync route registration + path-to-regexp v8
lib/oauth/index.jsExplicit session.save() before OAuth redirectSession auto-save timing changed in Express 5
lib/setup/session.jsUse redis (node-redis) client + named import for RedisStoreconnect-redis 9 requires node-redis client
DockerfileAdd python3 make g++ build toolsRequired for argon2 native compilation

New Dependencies Required

PackagePurpose
redis (node-redis) ^5.11.0Session store client for connect-redis 9

Recommendation

These changes are backward-compatible patterns (explicit await, explicit session.save()) that would also improve reliability on Express 4. The only breaking change is the connect-redis import/client switch and the path-to-regexp syntax. Would recommend the team to consider below changes for support of the new modules:

  1. Updating the core library files with the fixes above
  2. Adding redis (node-redis) as a dependency alongside ioredis
  3. Adding build tools to the Dockerfile template when argon2 is detected as a dependency
  4. Updating path-to-regexp syntax in route generation for Express 5 compatibility
1 Like

Thanks for the detailed report @Roney_Dsilva

The move to Express 5 is however quite a large undertaking and will a lot of breaking changes so we will have probably to do it first with a beta version of our server connect for NodeJS to test it out.

The argon2 peace is btw wrong - as it has already many precompiled binaries it is available for all platforms. And because the argon hashing is even natively available in node 24 it will get even easier and require no binaries.

So don’t trust the AI always :slight_smile:

3 Likes