Node.js's Config Hell Problem
Config often gets in the way of developer productivity when building with Node.js. But Deno’s zero config, batteries included approach means you can be productive immediately.
When you spin up a Node repo of any flavor, your root directory quickly fills
with config files. For example, in the latest version of Next.js, you get
next.config.js
, eslintrc.json
, tsconfig.json
, and package.json
.
Add styling, and you’ll have postcss.config.js
and tailwind.config.js
too.
Need middleware? Add middleware.ts
. Want error monitoring? Add
sentry.server.config.js
, sentry.client.config.js
, and
sentry.edge.config.js
. Plus, your env files, Git files, and your Docker
files…
Before you know it, your repo might look like:
All software requires configuration. You need some way to set up the projects, tools, plugins, and software you’re using. But how did we get to the point where we need 30 files to run one project? How did we find ourselves in Config Hell?
And how do we get out of it?
Config, but with smart defaults
Software is not one-size-fits-all — all users have slightly different needs. Configuration gives users flexibility to derive maximum value based on their use cases.
But config as the first step to using software is a bad user experience.
Take adding TypeScript to an existing Next.js project. First, we need to install TypeScript and the types:
npm install --save-dev typescript @types/react @types/node
Then we need to create our own tsconfig.json
:
touch tsconfig.json
Then what? If you’re just starting with TypeScript, you don’t know what configuration you want, so you do what any self-respecting developer would do: steal a config from Stack Overflow:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
Tired of manually adding TypeScript support to your projects? Try Deno, where TypeScript is natively supported.
And this is just to add TypeScript.
Effective software anticipates what its users are trying to accomplish by being opinioned with smart defaults. These “preset settings” are designed to provide an optimized experience for most users without the need for manual configuration. The settings are still available, but they’re only exposed when absolutely necessary.
Asking users to configure software before they can use it can damage goodwill and trust users have for your brand. Imagine you are using Gmail the first time and are greeted with this:
You’d suddenly be happy with your old hotmail account.
Smart defaults first; config later.
What are all those config files?
Let’s go back to that list above. What are all those files setting up?
- Ignore files (
dockerignore
,eslintignore
,gitignore
,prettierignore
,styleignore
): they’re used by tools to exclude certain files and directories from operations. They’re helpful to maintain a clean environment and efficient processes. - Run command files (
eslintrc.json
,lintstagedrc.json
,nvmrc
,nycrc
,stylelintrc.json
,prettierrc.json
,swcrc
): Run command (rc) config files specify settings or parameters when running certain commands, such aseslint
, lint-staged, and more. - Package files (
package.json
,yarn.lock
): these provide important information about dependencies and scripts for automation, allowing consistent management of the project’s environment. - Next.js files (
middleware.ts
,next-env.d.ts
,Next.config.js
,tsconfig.json
): these files manage the settings and configuration of a Next.js application. - Docker (
Dockerfile
,Dockerfile.deploy
,docker-compose.yml
): these files manage the configuration for automating the deployment and scaling of applications within containers. - Other (
editorconfig
,happo.js
,babel.config.js
,playwright.config.ts
,sentry.client.config.js
,sentry.server.config.js
,sentry.properties
, ): these files are configuration files that customize and manage various aspects of development environments, as well as third-party tools and libraries.
Next.js. Docker. Sentry. Happo. ESLint. npm. Yarn. Playwright. Babel. VSCode. SWC. Stylelint. Prettier. NVM. NYC. lint-staged. Git.
These are hardly esoteric tools. They’re a common collection of what you need to deploy a Next.js application to production. And to do so, you’ll need ~30 config files.
Why?
The JavaScript ecosystem is (mostly) unopinionated
Though today Node.js is primarily used to build websites and applications, it began with relatively modest intentions: to use event-driven architecture to enable asynchronous I/O. As Node’s popularity grew, JavaScript was suddenly used for everything: interacting with browser/DOM, the filesystem and Unix, build systems, bundling, transpiling, and so on.
The broad utility of JavaScript is reflected in the over 2 million modules on the npm registry. To be useful, JavaScript modules must support an increasing number of frameworks, meta frameworks, build tools, etc. to be able to slot into any project, for any workflow, in any situation. And the most direct approach is to keep the module “unopinionated” with an extensive config file that exposes complexities needed to work with any framework, tools, or stack.
As more tools are added to a Node.js project, config files not only devolve into cruft, but also impede developer productivity.
Making the complex simple
Software is a means to an end. Effective software gets out of the user’s way and lets them accomplish their task quickly.
Node.js, built as an asynchronous I/O, event-driven JavaScript runtime, didn’t anticipate it would be instrumental in revolutionizing web development (one out of every three new web pages or apps touches Node). But when a developer uses Node to build something new, they often spend a non-trivial amount of time pulling together their preferred stack and workflow — setting up TypeScript, their favorite testing framework, their favorite build process, etc.
But what if we were building for the web and could be productive immediately?
That’s how we approach building Deno, a web-native runtime with zero config and smart defaults so you start your next project and be productive immediately. It comes with native TypeScript support so you don’t need to spend time setting that up. Deno ships with a robust toolchain with built-in formatting, linting, testing, and more, so you don’t need to setup your own. Finally, Deno uses web compatible APIs, so if you’re already building for the web, you’re already familiar with Deno.
Programming is about managing complexity and making the complex simple is not requiring a configuration step.
🍋 Did you know? Fresh just got fresher.
Be sure to check out the release notes for Fresh 1.3, the latest iteration of the next-gen web framework for Deno.