-
Adding WDK to an existing Express API on Vercel - anyone else hit this wall?Hey everyone! I've been running a production Express API on Vercel for a while now, and recently wanted to add Workflow DevKit for some long-running processes (multi-day email sequences, client onboarding flows, etc.). The Problem I Ran IntoFollowing the docs, I set up Nitro with the After some digging, I figured out what was happening:
How I Got It WorkingI ended up writing a custom adapter that bridges Web API requests to Node.js format so Express can handle them normally. It's not pretty, but it works! import express from 'express';
import { Readable } from 'stream';
import type { IncomingMessage, ServerResponse } from 'http';
const app = express();
// ... your Express setup ...
/**
* Web API to Node.js Request Adapter
*/
function createNodeRequest(request: Request): IncomingMessage {
const url = new URL(request.url);
const readable = request.body
? Readable.fromWeb(request.body as any)
: Readable.from([]);
const req = Object.assign(readable, {
method: request.method,
url: url.pathname + url.search,
headers: Object.fromEntries(request.headers.entries()),
httpVersion: '1.1',
httpVersionMajor: 1,
httpVersionMinor: 1,
socket: {
remoteAddress: request.headers.get('x-forwarded-for') || '127.0.0.1',
encrypted: url.protocol === 'https:'
},
connection: {
remoteAddress: request.headers.get('x-forwarded-for') || '127.0.0.1'
}
}) as unknown as IncomingMessage;
return req;
}
/**
* Web API to Node.js Response Adapter
*/
function createNodeResponse(): { res: ServerResponse; promise: Promise<Response> } {
let statusCode = 200;
let statusMessage = 'OK';
const headers: Record<string, string | string[]> = {};
const chunks: Buffer[] = [];
let resolveResponse: (response: Response) => void;
const promise = new Promise<Response>((resolve) => {
resolveResponse = resolve;
});
const res = {
statusCode,
statusMessage,
headersSent: false,
writableEnded: false,
setHeader(name: string, value: string | string[]) {
headers[name.toLowerCase()] = value;
return this;
},
getHeader(name: string) {
return headers[name.toLowerCase()];
},
removeHeader(name: string) {
delete headers[name.toLowerCase()];
return this;
},
writeHead(code: number, message?: string | Record<string, string>, hdrs?: Record<string, string>) {
statusCode = code;
if (typeof message === 'string') {
statusMessage = message;
if (hdrs) {
Object.entries(hdrs).forEach(([k, v]) => { headers[k.toLowerCase()] = v; });
}
} else if (message) {
Object.entries(message).forEach(([k, v]) => { headers[k.toLowerCase()] = v; });
}
this.headersSent = true;
return this;
},
write(chunk: Buffer | string) {
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
return true;
},
end(chunk?: Buffer | string | (() => void), encoding?: string | (() => void), callback?: () => void) {
if (typeof chunk === 'function') { callback = chunk; chunk = undefined; }
else if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
if (chunk) chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
this.writableEnded = true;
const body = Buffer.concat(chunks);
const responseHeaders = new Headers();
Object.entries(headers).forEach(([k, v]) => {
if (Array.isArray(v)) v.forEach(val => responseHeaders.append(k, val));
else responseHeaders.set(k, v);
});
resolveResponse(new Response(body, {
status: statusCode,
statusText: statusMessage,
headers: responseHeaders
}));
if (callback) callback();
return this;
},
on() { return this; },
once() { return this; },
emit() { return false; },
flushHeaders() {}
} as unknown as ServerResponse;
return { res, promise };
}
/**
* Universal handler for both Web API and Node.js requests
*/
async function handler(input: Request | IncomingMessage, res?: ServerResponse): Promise<Response | void> {
if (input instanceof Request) {
const nodeReq = createNodeRequest(input);
const { res: nodeRes, promise } = createNodeResponse();
app(nodeReq, nodeRes);
return promise;
}
return app(input, res!);
}
export default handler;Nitro Config// nitro.config.ts
import { defineNitroConfig } from 'nitro/config'
export default defineNitroConfig({
preset: 'vercel',
entry: './src/index.ts',
vercel: { entryFormat: 'node' },
modules: ['workflow/nitro'],
externals: { external: ['@prisma/client', '.prisma/client'] }
})How It Works
What's Working NowWith this adapter in place, everything works together:
It's been running in production for a bit now with ~30+ Express routes plus workflow endpoints, and so far so good. Did anyone else run into this?I'm curious if others have tried adding WDK to an existing Express app on Vercel:
Would love to hear if the WDK team has thoughts on better Express integration for Vercel deployments. Happy to turn this into a PR if there's interest in adding something like this to the official toolkit. Ifeanyi Onubogu |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 17 replies
-
|
hey @waldhari1! thanks for mentioning this. just checked the docs and it seems that they're wrong. i was able to repro this issue and the following fixed it, can you try this? import { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
modules: ["workflow/nitro"],
vercel: { entryFormat: "node" },
routes: {
"/**": "./src/index.ts" ,
},
});Let me know if that helps! |
Beta Was this translation helpful? Give feedback.
-
|
@eersnington @adriandlam upvote this discussion let's see if other devs would see and contirbute so we can all get to the bottom of this |
Beta Was this translation helpful? Give feedback.
@adriandlam BINGO🎉🎉 it worked!
Our Express + Nitro + Vercel deployment is now live and functioning correctly. The "Executing Node.js middleware in an edge runtime" error is gone.
For anyone else hitting this issue, here's the working config:
And in
src/index.ts, export the Express app directly:Thanks so much for the quick fix! This unblocks our Workflow DevKit integration which relies on the Express app structure.