Skip to main content

How to apply modern Node.js patterns in real projects

Learn how to apply modern Node.js patterns by setting ESM as the module system, splitting modules by responsibility, and using middleware effectively.

Kodetra TechnologiesKodetra Technologies
11 min read
Sep 8, 2026
0 views
How to apply modern Node.js patterns in real projects

TL;DR

  • Set ESM as the project module system first.
  • Split modules by responsibility for clarity.
  • Add middleware only where a request needs stages.
  • Remove unnecessary external dependencies.
  • Verify changes with the built-in test runner.

Start by checking four prerequisites before you touch structure. Modern Node.js Patterns for 2025 signals that ES Modules (ESM) are now the clear winner in Node.js module systems. This matters because imports, exports, file layout, and tool behavior all depend on one module system being chosen first. If your goal is the Modular Pattern, define the boundary before you move files around.

Node.js setup checks before you change any code

Start by checking four prerequisites before you touch structure. You need a current Node.js runtime installed [unverified], a small project you can run end to end [unverified], package settings that clearly state which module system the project uses [unverified], and a written target for the refactor such as “move file loading behind one module” [unverified].

According to Modern Node.js Patterns for 2025, ES Modules (ESM), the standard JavaScript module system using import and export, have become the clear winner in Node.js module systems. That matters before a refactor because imports, exports, file layout, and tool behavior all depend on one module system being chosen first [unverified].

Do not mix module systems blindly.

If your goal is the Modular Pattern, meaning breaking an application into independent, self-contained modules, define the boundary before you move files around. Pick one concrete change, such as isolating database access or configuration loading, so you can tell whether the refactor improved the code or just shuffled it [unverified].

Quick setup checklist

  • Confirm the app starts and completes one useful path without errors [unverified].
  • Confirm your package settings and source files agree on one module system [unverified].
  • Confirm one target area for the refactor and write it down in a sentence [unverified].
  • Confirm you can roll back easily with version control or a copy of the project [unverified].

Troubleshooting FAQ

Q: import fails even though the file exists. A: Your project is likely configured for a different module system than the code you wrote; make the package setting and file syntax match before changing architecture [unverified].

Q: require works in one file but not another. A: The codebase is probably mixing module systems; standardize on one approach first, because module boundaries depend on consistent loading rules [unverified].

Q: I created more files, but the code feels harder to follow. A: You changed file count without defining module responsibilities; apply the Modular Pattern by giving each module one clear job and a small public surface.

Q: I am not sure whether I should refactor yet. A: If you cannot name the target change, stop and define it first; this material is not aimed at beginners, so a vague goal turns setup into guesswork.

Node.js migration steps from old habits to clean modules

  1. 1. Set ESM as the project module system first, then stop mixing styles. Use import and export in the files you touch, because the new import syntax improves on earlier complex string syntaxes used in module loaders. According to the Node.js article on modern patterns, this is the clean base that makes later changes simpler to verify.
  1. 2. Check that each converted file has one clear loading style and one clear public surface. A file passes this step when it exports only the values another file should use, and all internal helpers stay private [unverified]. If a file still mixes old and new loading forms, it fails this step [unverified].
  1. 3. Split modules by responsibility, meaning one file should own one kind of work. Put request parsing in one place, data access in another, and business rules in another [unverified]. You can check this by asking whether a file has one reason to change; if it handles unrelated work, split it again [unverified].
  1. 4. Replace “utility” grab-bags with named modules that describe what they do. A file named for dates, auth, or orders is easier to place than a file named helpers or utils [unverified]. This step is done when each shared module has a narrow purpose you can explain in one short sentence [unverified].
Old choiceModern choiceWhat to check
Mixed module stylesESM with import/exportOne loading style per file
Large feature filesSmall modules by responsibilityOne reason to change per file
Logic inside route handlersStaged request processingShared steps moved before final handler
Extra test packages by defaultBuilt-in test runnerRemove unneeded test dependencies
  1. 5. Add middleware only where a request needs stages. Middleware is a pipeline where incoming requests pass through several processing stages before reaching the final handler. Use it for work that should happen before the final handler, such as reading auth state, validating input, or attaching shared request data [unverified].
  1. 6. Keep middleware thin and ordered. A middleware step should either add information to the request, reject the request, or pass control onward [unverified]. This step is checkable when each stage has one job and the final handler no longer repeats that job across routes [unverified].
  1. 7. Move repeated request checks out of handlers and into shared middleware. If three handlers all parse the same header or reject the same bad input, that logic belongs in the pipeline before the final handler [unverified]. You are done when handlers mostly decide the response, not the setup work [unverified].

A practical migration pass

  1. 8. Convert one feature at a time, not the whole codebase at once. Pick a route or command, switch its modules to ESM, split oversized files, and move repeated request stages into middleware [unverified]. This keeps failures local and makes review easier because each change has one theme [unverified].
  1. 9. Use the built-in test runner before adding more test tooling. Node.js now includes a full-featured test runner that covers most testing needs without any external dependencies. This step is complete when your common tests run there and you have not added another package for work the built-in runner already does.
  1. 10. Reduce external dependencies after each feature pass. If a package only fills a gap that Node.js now covers directly, remove it and update the code to the built-in path. This cuts package surface area, which means fewer updates to track and fewer places for configuration drift to hide [unverified].

One less package is one less package to audit.

  1. 11. Keep only dependencies that still earn their place. A dependency stays when it provides a clear capability you are actively using and cannot replace with the platform without worse code [unverified]. A dependency goes when it duplicates built-in testing support or props up old module habits you have already removed.
  1. 12. Mark the migration done per feature with a short checklist. The feature should use ESM, have modules split by responsibility, use middleware only for staged request work, and rely on the built-in test runner when that is enough. If any item fails, the feature is not finished, even if the code already runs [unverified].

Node.js failure signs and the fix for each one

When a migration breaks, read the first failing file and the first thrown error before you touch anything else.

According to Modern Node.js Patterns for 2025, ES Modules or ESM are now the clear winner in Node.js module systems because they match web standards and get better tooling support. That matters when failures look random, because many of them are really one problem: your files, imports, and package settings are not all speaking the same module system.

Troubleshooting FAQ

Q: Cannot find module appears right after you changed require to import. A: Check the import path first. In ESM, path mistakes often show up immediately because the runtime resolves the exact file you named [unverified]. Fix the path so it matches the real file and make sure your package settings and file style agree on ESM instead of mixing systems.

Q: One file uses import, another uses module.exports, and behavior is inconsistent. A: You have mixed CommonJS and ESM. CommonJS is the older Node.js module format built around require and module.exports [unverified], while ESM uses import and export. Pick one direction for the module boundary and convert the module pair together, because partial conversion leaves each side expecting different shapes and load rules [unverified].

Q: Tests stopped running, or the test command finds nothing. A: Start by removing test-runner assumptions from the old stack. Node.js includes a built-in test runner, which is the tool that discovers and runs test files, and it covers most testing needs without external dependencies. If discovery changed during migration, align your test files and invocation with the built-in runner instead of keeping old conventions by accident.

Q: The app runs, but changing one module breaks unrelated parts. A: Your modules are still too tightly coupled. The Modular Pattern means breaking the application into independent, self-contained modules. Split shared state and side effects away from logic so each module has one job and a smaller surface for failures.

Node.js checks that prove the new patterns worked

  1. 1. Put the body image right before this section in your layout so the visual break marks the switch from migration to proof [unverified]. Then verify module loading by starting the app from the same entry point you expect in normal use and from the test command you expect in automation [unverified].
  1. 2. Watch for one result: every import path resolves the same way in both runs, with no special-case loader behavior and no file that only works in one context [unverified]. If one path fails, the pattern did not stick, because consistent loading is the first sign that the boundaries between files are now plain and repeatable [unverified].
  1. 3. Run the built-in test runner, meaning the test tool that comes with Node.js itself, against the modules you changed [unverified]. The check is simple: tests pass without hidden bootstrapping, and each test can load only the code it needs instead of dragging in the whole app [unverified].
  1. 4. Add request logging in middleware, which is code that runs between receiving a request and sending a response, so you can see the request flow end to end [unverified]. Verify that each request leaves a readable trail through validation, business logic, and response handling, because visible flow is how you confirm responsibilities are separated instead of tangled [unverified].

That trace should read like a handoff, not a maze [unverified].

  1. 5. Change one rule in one module and rerun the same tests [unverified]. If only nearby tests need updates, your design is easier to change; according to the new edition announcement, modular design and dependency injection are core practices for complex systems.
  1. 6. Treat Publish-Subscribe (Pub/Sub) as a pattern for distributed systems, where one part publishes messages and other parts subscribe to receive them, not as your default inside one process. Verify that your current boundary does not need Pub/Sub unless work actually crosses process or service lines; if it does not, keep the simpler in-process event flow.

Verification signals

SignalWhat it meansWhat to do if missing
Same module loads in app run and test runImports are consistent across contextsCheck entry points, import paths, and any environment-specific loader setup [unverified]
Built-in runner passes without extra boot codeTests exercise modules directlyRemove hidden global setup and pass dependencies in explicitly [unverified]
One request produces a clear middleware trailRequest flow is visible and orderedAdd logging at each boundary and keep each middleware focused on one job [unverified]
A small rule change touches a small areaResponsibilities are separatedSplit mixed files by role and move wiring to the edge of the app [unverified]
No pressure to add Pub/Sub inside one processBoundaries match the problemReserve Pub/Sub for distributed cases only
  1. 7. Do one last pass for maintenance signals rather than runtime signals [unverified]. On September 5, 2024, the book announcement also reported more mentions of TypeScript and expanded security coverage, which is a useful reminder to verify types and safety checks where your new module boundaries meet input and output.

Node.js patterns pay off when your app starts changing

The payoff shows up when the app stops being small and stable. Node.js is used for web apps, APIs, microservices (small services that do one job), real-time systems, and cloud-native workloads, so change is the normal case, not an exception.

When a new feature lands, a clear pattern gives you one obvious place to put code, one obvious way to pass data, and fewer accidental links between parts. When the team changes, that same structure cuts handoff time because people can read intent from the shape of the code instead of guessing from side effects.

**Why it matters:** If your backlog keeps shifting, patterns stop each request from turning into a special case. They help you change one part without quietly breaking three others.

According to the Node.js Design Patterns team, guidance like this has been tracked since 2014 and has helped tens of thousands of developers worldwide.

You are not following fashion; you are buying cheaper changes, calmer onboarding, and tighter control over dependencies.

Your setup is modern enough if these checks pass

Q: What is the clearest sign the setup is modern enough? A: The app and tests load the same modules the same way. If ESM works from the normal entry point and from the built-in test runner, the base is correct.

Q: How do I know my modules are split well enough? A: Change one rule in one module and watch the impact. If only nearby tests and files need updates, your module boundaries are doing their job.

Q: When does middleware count as a good fit? A: Use middleware only for staged request work such as validation, auth checks, or attaching shared request data. If handlers still repeat setup work, the split is not finished.

Q: Should I add more packages to finish the migration? A: No. Keep dependencies only when they provide a capability Node.js does not already cover cleanly, and remove packages that duplicate built-in testing or support old module habits.

Sources