HN user

colinmcd

1,410 karma

Creator of Zod Formerly at EdgeDB and Bun Ride-or-die TypeScripter

Posts63
Comments144
View on HN
nubjs.com 12d ago

We built a 5x faster JavaScript package manager

colinmcd
2pts0
nubjs.com 14d ago

Node.js's phantom dependency problem and the path to a 5x faster package manager

colinmcd
1pts0
github.com 28d ago

Show HN: Nub – A Bun-like all-in-one toolkit for Node.js

colinmcd
277pts81
github.com 29d ago

Nub – A Bun-like toolkit that extends Node.js instead of trying to replace it

colinmcd
3pts0
bolt.new 11mo ago

Bolt Cloud

colinmcd
1pts0
zod.dev 2y ago

An experiment in fixed-term open-source patronage

colinmcd
3pts0
www.lerf.io 3y ago

Language Embedded Radiance Fields

colinmcd
7pts0
bun.sh 3y ago

Bun v0.4

colinmcd
2pts0
www.edgedb.com 3y ago

Show HN: TypeScript query builder with full type inference

colinmcd
4pts0
www.edgedb.com 4y ago

Designing the ultimate TypeScript query builder

colinmcd
5pts0
www.edgedb.com 4y ago

Designing the ultimate TypeScript query builder

colinmcd
1pts0
www.edgedb.com 4y ago

Sharding our test suite for 10x faster runs on GitHub Actions

colinmcd
4pts0
www.edgedb.com 4y ago

Everyone is wrong about ORMs

colinmcd
6pts4
www.edgedb.com 4y ago

A solution to the SQL vs. ORM dilemma

colinmcd
2pts0
www.edgedb.com 4y ago

Why ORMs are slow – and getting slower

colinmcd
9pts0
www.edgedb.com 4y ago

How slow are ORMs, really?

colinmcd
3pts1
github.com 4y ago

Show HN: IMDBench – Benchmarking ORMs with realistic queries

colinmcd
4pts0
www.edgedb.com 4y ago

The graph-relational database, defined

colinmcd
3pts0
www.edgedb.com 4y ago

The graph-relational database, defined

colinmcd
19pts8
www.edgedb.com 4y ago

Show HN: EdgeDB 1.0

colinmcd
947pts323
www.edgedb.com 4y ago

We can do better than SQL

colinmcd
3pts1
www.edgedb.com 4y ago

EdgeDB Release Candidate 3 is now available

colinmcd
2pts0
www.edgedb.com 4y ago

EdgeDB Release Candidate 2

colinmcd
2pts1
www.edgedb.com 4y ago

EdgeDB 1.0 Release Candidate

colinmcd
3pts0
www.edgedb.com 4y ago

EdgeDB 1.0 Release Candidate

colinmcd
6pts1
colinhacks.com 4y ago

Building a strongly typed API without GraphQL

colinmcd
1pts1
www.fastcompany.com 4y ago

SweetGreen acquires robotic kitchen startup Spyce

colinmcd
1pts0
www.edgedb.com 4y ago

EdgeDB Beta 3

colinmcd
4pts3
colinhacks.com 5y ago

API typesafety without the tears (or code generation)

colinmcd
2pts3
colinhacks.com 5y ago

The dream of end-to-end typesafety

colinmcd
2pts0

You can continue using pnpm if you want. Nub is a la carte. Nub's runtime or script runner functionality will work with any package manager that uses node_modules (actually Yarn PnP is also supported out of the box).

You can also move over to Nub from pnpm incrementally if you want the performance or security wins. It has complete pnpm compatibility: flag for flag identical CLI, reads and writes from your pnpm-lock.yaml, and respects your expecting configs completely. Nub and pnpm can be used simultaneously by different teammates without lockfile churn. This is a key design goal, and its what I mean by "incrementally adoptable". But yeah `nub install` will almost certainly work in your repo out of the box. (File an issue otherwise.)

https://nubjs.com/docs/install/pnpm

Same is true for npm, yarn, and bun as well. It detects your current package manager (based on package.json#packageManager or lockfile existence) and runs in "compatibility mode" for that specific package manager.

https://nubjs.com/docs/install#config-it-reads

Exactly this. TypeScript also ended up being many users' entrypoint to JSX, decorators, etc as it became a catchall transpiler surface so respecting tsconfig is vital here (Node understandably has a policy against this). Plus there are extensionless imports, the "phantom" .js imports (pointing to .ts files), etc. List goes on. Type stripping ends up working out of the box for virtually no real world projects sadly.

https://nubjs.com/docs/runtime/typescript

Indeed, Nub intentionally introduces no Nub-specific APIs: no Nub global, no nub: prefixed built-in modules, no Nub-named config file / lockfile, no "nub" field in package.json, not even any NUB_ environment variables. Most of the stuff Bun added is better as a proper dependency imo.

EDIT: Sorry, I understand you're talking about package.json. Would be fun to try to get the Node & package mgmt teams aligned to add support for comments in the package.json. Bun tried and failed to do this (requires ecosystem coordination).

Nub could absolutely support a config file and use it to set NODE_OPTIONS or flags in the node child process. There's no reason to throw out the baby with the bathwater due to DX concerns like this. That's a key part of the concept Nub is trying to prove. (To be clear I'm quite content to conform to Node's no-config-file policy at the moment.)

Right now, you should use Nub on the backend if you are relying on its augmentations. If you specifically want to disable Nub's augmentations (so you have a guarantee that your app/script will "just work" with regular Node, there's a couple ways to disable it.

  NODE_COMPAT=0 nub index.ts
  nub --node index.ts

I'll investigate a `nub build` that would do the transpilation upfront and properly chunk/bundle a prod build. It's a good idea. But yes, Nub's overhead (both time and space) is generally negligible relative to Node itself.

Re: added attack surface: the most obvious one is that Nub loads .env files (same as Bun/Next/Vite) so be aware of that. All of Node's permission flags are passed through as well. I won't claim there's no additional attack surface, but it doesn't have much surface area, just a Rust wrapper that spawns `node` ultimately.

We use this to register our preload purely for performance reasons. In this and many other cases CommonJS is still faster than ESM. Using --require is about 0.5ms overhead vs 4.6ms for --import (on my M1 Macbook Pro).

Relatedly Node.js recently (2025) introduced a synchronous version of its resolver hook registration API (`module.registerHooks()`) specifically to improve performance over the old async `module.register()` API. It was a big unblocker for Nub. For the interested, the async API added 19ms fixed registration overhead + about 130us additional overhead per import.

Which flag Nub uses here doesn't impact userland at all, TLA is supported wherever it's supported by Node.js itself.

Cloudflare Workers is a different runtime and has its own toolchain around it. Nub could theoretically support it when executing files (spawn `wrangler dev` instead of `node` if wrangler.toml is detected or something) but really I'm focused on making the Node.js experience as good as possible.

The other pieces of the toolkit could absolutely be used: package manager, script runner, package runner. Works with anything that implements the Node module resolution algorithm (actually Yarn PnP also works out of the box...).

Zod 4 1 year ago

As you allude to: your aliased "zod-next" dependency wouldn't be able to satisfy the requirements of any packages with a peer dep on Zod. But this approach has a more fundamental flaw. My goal is to let ecosystem libraries support Zod 3 and Zod 4 simultaneously. There's no reliable way to do that if they aren't in the same package.[0]

[0] https://github.com/colinhacks/zod/issues/4371

Zod 4 1 year ago

It won't typecheck, which is good in this case, because as you say that's a very bad idea :)

Zod 4 1 year ago

I understand this as a knee-jerk reaction. I didn't do this lightly.

Perhaps publish a 4.x along with the 3.x stuff

You have some misconceptions about how npm works. Unfortunately it's less reasonable than you think. There's a single `latest` tag, and there's only one "latest" version at a time. It's expected that successive versions here will follow semver. Once I publish zod@4 I can no longer publish additional zod@3.x.x versions. The workaround here is to publish v3 versions to a separate dist tag (zod@three) but anyone consuming that dist-tag (e.g. "zod": "three" in their package.json) loses the ability to specify a semver range.

I recommend reading the writeup[0]. I don't think you're appreciating the magnitude of the disruption a simple major version bump would have caused, or the reasons why this approach is necessary to unlock continuity for Zod's ecosystem libraries. They're quite subtle.

[0] https://github.com/colinhacks/zod/issues/4371

Zod 4 1 year ago

No, that kind of interop, especially static interop (assignability), would've been totally unworkable. Despite the length of the changelog, there are very few breaking changes to the user-facing API surface. It's mostly internal/structural changes and deprecations (most of which can be fixed with a find&replace).

Report back about that .d.ts issue. It should be far better. That kind of type explosion usually happens when TypeScript needs to infer function/method return types. Zod 4 uses isolatedDeclarations so this kind of thing shouldn't happen.

Zod 4 1 year ago

This is not done for Zod's benefit. It's done for the benefit of libraries that depend on Zod, and the users of those libraries. If a library wants to add "incremental" support for Zod4 (that is, without dropping support for Zod 3 and publishing a new major), they need access to types (and possibly runtime code) from both libraries to do so in a sound way. I detail a number of other approaches for achieving this here[0] and why they ultimately fall short. Ultimately npm wasn't designed for this particular set of circumstances.

[0] https://github.com/colinhacks/zod/issues/4371

Zod 4 1 year ago

Some kind of affordance for FormData/URLSearchParams-style structures is definitely in scope. It was a late cut. Ultimately HTML inputs/forms are an implicit type system unto itself—certainly HTML has a very different notion of "optional" than TypeScript. So my approach to this would likely involve another sub-library ("zod/v4-form").

I like your library! Put in a PR to add it to the ecosystem page :)

Zod 4 1 year ago

Ecosystem libraries would need to switch from a single peer dependency on Zod to two optional peer dependencies. Despite "optional peer dependencies" technicall being a thing, its functionally impossible for a library to determine which of the two packages your user is actually bringing to the table.

Let's say a library is trying to implement an `acceptSchema` function that can accepts `Zod3Type | Zod4Type`. For starters: those two interfaces won't both be available if Zod 3 and Zod 4 are in separate packages. So that's already a non-starter. And differentiating them at runtime requires knowing which package is installed, which is impossible in the general case (mostly because frontend bundlers generally have no affordance for optional peer dependencies).

I describe this in more detail here: https://x.com/colinhacks/status/1922101292849410256

Zod 4 1 year ago

Author here, AMA!

Regarding the versioning: I wrote a fairly detailed writeup here[0] for those who are interested in the reasons for this approach.

Ultimately npm is not designed to handle the situation Zod finds itself in. Zod is subject to a bunch of constraints that virtually no other libraries are subject to. Namely, the are dozens or hundreds of libraries that directly import interfaces/classes from Zod and use them in their own public-facing API.

Since these libraries are directly coupled to Zod, they would need to publish a new major version whenever Zod does. That's ultimately reasonable in isolation, but in Zod's case it would trigger a "version avalanche" would just be painful for everyone involved. Selfishly, I suspect it would result in a huge swath of the ecosystem pinning on v3 forever.

The approach I ended up using is analogous to what Golang does. In essence a given package never publishes new breaking versions: they just add a new subpath when a new breaking release is made. In the TypeScript ecosystem, this means libraries can configure a single peer dependency on zod@^3.25.0 and support both versions simultaneously by importing what they need from "zod/v3" and "zod/v4". It provides a nice opt-in incremental upgrade path for end-users of Zod too.

[0] https://github.com/colinhacks/zod/issues/4371

Zod 4 1 year ago

These numbers don't reflect anything useful. This is the total size of the code in the package, most of which will be tree-shaken. In Zod's case, the package now contains three independent sub-libraries. I recommend plugging a script into bundlejs.com[0] to see bundle size numbers for a particular script

[0] https://bundlejs.com

Zod 4 1 year ago

Author here. I wrote a fairly detailed writeup here[0] for those who are interested in the reasons for this approach.

Ultimately you're right that npm doesn't work well to manage the situation Zod finds itself in. But Zod is subject to a bunch of constraints that virtually no other libraries are subject to. There are dozens or hundreds of libraries that directly import interfaces/classes from "zod" and use them in their own public-facing API.

Since these libraries are directly coupled to Zod, they would need to publish a new major version whenever Zod does. That's ultimately reasonable in isolation, but in Zod's case it would trigger a "version avalanche" would just be painful for everyone involved. Selfishly, I suspect it would result in a huge swath of the ecosystem pinning on v3 forever.

The approach I ended up using is analogous to what Golang does. In essence a given package never publishes new breaking versions: they just add a new subpath when a new breaking release is made. In the TypeScript ecosystem, this means libraries can configure a single peer dependency on zod@^3.25.0 and support both versions simultaneously by importing what they need from "zod/v3" and "zod/v4". It provides a nice opt-in incremental upgrade path for end-users of Zod too.

[0] https://github.com/colinhacks/zod/issues/4371

We are releasing a proper announcement later! This video was scheduled to premiere at 10am but due to some last minute bugs (and Murphy's Law) the build didn't finish in time. We decided not to delay the premiere because it was already on calendars and whatnot.

This video is definitely corny. (I'm in it.) This is intended as an extremely accessible introduction to Bun, definitely not targeted at the HN crowd. I think we really just wanted to make something with some production value to reflect the fact that Bun itself is post-1.0 and more stable/polished.

The two camera thing is a classic trick that lets you add some visual interest. Basically you're cutting between two angles. And you can only be looking directly at one camera at a time so you won't be looking into the lens for one of the two shots.

Colin here, I work on Bun.

Bun 1.0 is actually still compiling but we decided not to delay the premiere of this announcement video since it was already on calendars. We'll be publishing a full announcement post later today that's more detailed than this video.