Runtime First: Designing APIs Without the Crutch of Bundlers and Static Analysis
In modern JavaScript development, we’ve grown comfortable with bundlers, compilers, and type generators. Tools like Webpack, Vite, Rollup, Babel, and TypeScript have become so deeply ingrained in our workflow that we often assume they’ll always be there to help us optimize and fix problems before our code hits the runtime.
But this reliance comes at a cost. Designing APIs and libraries that depend on static analysis or build-time transformations can lead to hidden coupling, brittle systems, and polluted design choices that ripple across entire codebases. The philosophy of “Religiously Runtime” is a response to this creeping complexity.
Let’s unpack this philosophy, its benefits, trade-offs, and how to apply it effectively.
The Core Idea: Let Runtime Reign
At its heart, “Runtime First” means every line of code, every API, and every test should work as-is, without relying on build tools. That means:
- No assumptions about tree-shaking—if you don’t use something, it’ll still be there at runtime unless explicitly removed.
- No reliance on TypeScript for correctness—your code must handle errors and types robustly at runtime.
- No special treatment by bundlers or compilers—your code must work when directly run by Node.js, Deno, or a browser environment.
It’s a simple but radical idea: design your software as if there were no pre-runtime safety nets.
Why Static Analysis Can Pollute APIs
When we rely on static analysis (like TypeScript, tree-shaking, or compile-time plugins), we tend to:
- Introduce subtle coupling: Certain exports or features only work because the bundler removes the rest.
- Split APIs into “development” and “production” versions, where things that are stripped away or optimized might not even exist in runtime.
- Assume correctness because “the type checker passed” or “the bundler optimized it out.”
This pollutes the design. A runtime-first approach avoids this by forcing APIs to be explicit and complete, regardless of the environment.
How to Embrace Runtime-First Design
Here are the principles and practices for designing without static crutches:
✅ Design Explicit APIs: Avoid re-exports, conditional exports, or deep, intertwined module structures. Each module should be clear and self-contained.
✅ No Assumptions About Bundlers: Your library should work even if consumed in a runtime like Node.js, Deno, or a browser module loader, without requiring bundling or tree-shaking.
✅ Use TypeScript and JSX Only as Loaders: It’s fine to use TypeScript or JSX for simple syntax transformation (e.g., stripping types, transpiling JSX), but not for correctness or analysis.
✅ Write Tests Without Bundling: Run your tests directly in a runtime environment, like Node.js or a browser using native ESM, to ensure they reflect real-world behavior.
✅ Runtime Validation Over Type Checking: If you need to enforce types or contracts, validate them at runtime using tools like zod
, io-ts
, or custom validation logic.
✅ Don’t Depend on Side Effects: Avoid global state, import-time side effects, or patterns that assume a specific bundler behavior.
The Benefits
By adopting a runtime-first approach, you gain:
🔒 Stronger Correctness: Your code behaves exactly as it will in production—there’s no hidden reliance on build tools.
🔄 True Portability: Your libraries can run in any JavaScript environment (Node.js, Deno, browsers) without special build steps.
🔎 Improved Clarity: APIs are explicit and self-contained, easier to reason about and maintain.
🚀 Simpler Dev Environments: You can run code and tests with a simple node
command or directly in a browser, no build steps needed.
Potential Trade-offs
Of course, this approach comes with challenges:
⚖️ Bundle Size Concerns: Without tree-shaking or dead code elimination, your libraries may be larger. Mitigate this by careful modular design, making sure consumers can import just what they need.
🧩 Manual Runtime Validation: Without TypeScript checking at compile-time, you’ll need to write comprehensive runtime checks—but these can be encapsulated in utility functions or validators.
📚 Ecosystem Resistance: Many libraries and frameworks are deeply tied to static tooling. You might need to avoid or carefully integrate them.
Additional Considerations
Here are points you might not have considered:
- Debuggability: Debugging runtime-first code is often easier because there’s no transformed stack trace or generated code—just the original source.
- Security: By relying on runtime validation and explicit logic, you reduce the attack surface introduced by implicit build-time assumptions.
- Longevity: As JavaScript ecosystems evolve, build tools come and go. Runtime correctness stays.
- Developer Training: Your team may need to unlearn old habits—like “oh, the bundler will handle that”—and think in terms of runtime behavior.
Finally
“Religiously Runtime” is not about rejecting tools like TypeScript or bundlers—it’s about rejecting dependency on them. It’s a call for clarity, correctness, and resilience in system design, where APIs stand on their own feet and behavior is consistent in any environment.
If we embrace this philosophy, we’ll build cleaner, stronger, and more reliable software, ready to handle the challenges of evolving runtimes and deployment scenarios.
Comments ()