node-ipc-bridge
Some of the most useful things I've built exist to solve a problem I created for myself by wanting to use Python and Node in the same project.
Some of the most useful things I've built exist to solve a problem I created for myself by wanting to use Python and Node in the same project. Python has the machine learning ecosystem. Node has the ergonomics I actually want to build a UI or a server in. Refusing to choose between them means you eventually need the two processes to talk to each other, and "talk to each other" turns out to be a surprisingly under-served problem once you get past "just use a REST API between them," which works but adds an HTTP server, a port, and a network round trip to what's often just two processes running on the same machine that need to pass messages back and forth quickly.
node-ipc-bridge is the message-passing layer I built instead. A newline-delimited JSON protocol, one implementation in TypeScript for the Node side, one companion implementation in plain Python for the other side, both speaking the exact same wire format so a Node process and a Python process can call each other's functions like they're local, without either of them needing to run a web server just to say hello.
How the protocol actually works
Every message is a JSON object with an id, a type (request, response, event, or error), and depending on type, a method, params, result, or error field. Requests get correlated to responses by that incrementing numeric ID, the same pattern every RPC system since forever has used, because it's simple and it works. call() on the Node side returns a promise that resolves when the matching response comes back, with a timeout so a hung Python process doesn't leave you waiting forever. send() is the fire-and-forget version: no response expected, no correlation needed, useful for one-way notifications where you don't care about an acknowledgment.
Two transports are supported: spawning the other process directly as a child process and talking over its stdin/stdout, or connecting over a raw TCP socket if the two sides are already running independently and just need to find each other. The default spawn command is literally the string "python", not a placeholder, not an example, the actual default, which tells you plainly what this was built for. I wasn't designing an abstract cross-language bridge for hypothetical future use cases. I was bridging a specific Node process to a specific Python process, and I built the default to match exactly that.
The Python half, and where the README goes quiet about it
The node_ipc_bridge.py file sitting at the repo root is the other half of the same protocol, written in plain Python, with no dependencies beyond the standard library: json, sys, traceback. It exposes a Bridge class with a @bridge.method() decorator for registering handlers, and reads from stdin, writes to stdout, in the same envelope format the TypeScript side expects. It is not published to npm. It's not even referenced in package.json's file list. It just sits there, ready to be copied into whatever Python project needs to speak the other end of this protocol, which its own docstring says outright: "drop this file into your Python project."
Here's the part I have to be honest about: the README's usage example only shows Node talking to Node. Two Node processes, worker.js on one end, calling each other over the bridge. Nowhere in the README does it mention Python, or the .py file, or the actual cross-language use case that's sitting in the package.json description as the stated purpose of the whole thing. The real instructions for the Python side live only in that file's own docstring, not in the README anyone would actually read first. It reads like the Python companion got added after the initial release, quickly, to make the documented use case (Node talking to Python) actually true, and then nobody went back to update the README to mention it exists. Which is close to exactly what happened.
Why I'm not embarrassed by that gap
I could smooth this over and describe it as though it was always the plan. It wasn't, not cleanly. I built the Node-to-Node version first because that was the easier problem to prove out: get the envelope format right, get request/response correlation right, get the timeout logic right, all without the added complexity of a second language. Once that worked, adding the Python side was mostly a matter of translating the same envelope logic into a different syntax, which took an afternoon, not a redesign. The gap in the README isn't a lie. It's a snapshot of documentation that stopped being updated the moment the code outgrew it, which is an extremely common failure mode for solo projects and one I'd rather name plainly than pretend never happens to me.
Where it's actually load-bearing
Efath's web scraper leans on a version of this exact pattern: a Node-adjacent orchestration layer reaching into Python where the actual scraping and AI-summarization logic lives, because Python's ecosystem for that kind of work is simply better resourced than JavaScript's equivalent. node-ipc-bridge is the general-purpose version of a bridge I kept needing to rebuild slightly differently for each project, and packaging it once means the next time I need Node and Python in the same system, the message-passing layer is a dependency, not a rewrite.
The timeout decision that took longer to get right than the rest of the protocol combined
Every call() needs a timeout, because a Python process can hang (waiting on a model to load, stuck in a blocking call, crashed without exiting cleanly), and a Node process that just waits forever for a response that's never coming is a worse failure mode than one that gives up after a reasonable window and says so clearly. Picking that window took more iteration than everything else in the protocol combined, because the right number depends entirely on what the other side is actually doing. A quick data-lookup call should time out in a second or two. A call that triggers a model loading from disk for the first time might legitimately need thirty. I ended up making the timeout a per-call option rather than a single global default, which means every call site has to actually think about how long its specific operation should reasonably take, instead of inheriting one number that's wrong for most of the calls that use it. More typing at each call site. Fewer mysterious hangs and fewer premature timeouts killing something that just needed a few more seconds.
Why newline-delimited JSON instead of a length-prefixed frame
I considered a proper length-prefixed binary framing for the wire protocol at one point: send the message length as a fixed-size header, then exactly that many bytes of JSON, which avoids any ambiguity about where one message ends and the next begins. I didn't build it that way. Newline-delimited JSON is simpler to implement on both sides, trivially debuggable by eye if you're staring at raw stdout during development, and completely sufficient as long as nothing you're serializing contains a literal unescaped newline, which for JSON it never does, because JSON's own encoding escapes newlines inside strings automatically. The simpler format cost me nothing in practice and saved real implementation complexity on both the Node and Python sides. Not every protocol decision needs to anticipate every theoretical edge case. This one just needed to work for JSON specifically, and newline-delimited framing is exactly sufficient for that one job.
Full stack developer. Founder of Yashveer Labs. Two languages, one protocol, and a README that's still catching up to the code.
Start a conversation about this.
Whether it's node-ipc-bridge itself or the next system worth building, the lab is reachable.