Question

require() vs import in Node.js: Understanding CommonJS and ES Modules

typescriptnode.jsimportrequire

Question

I am starting a small Node.js project that connects to MongoDB, and I am having trouble importing installed packages correctly.

For example, this code throws an error saying that express has no default export:

import express from "express";

But this version works:

const express = require("express");

What is the difference between import and require() in Node.js? Also, why might import express from "express" fail while require("express") works, and how can I configure my project so imports behave correctly?

Short Answer

By the end of this page, you will understand why require() and import are not interchangeable in every Node.js project, how CommonJS and ES Modules differ, why errors like “has no default export” happen, and how to choose and configure the right module system for your project.

Concept

Node.js has used two module systems:

  • CommonJS (CJS) uses require() and module.exports
  • ES Modules (ESM) use import and export

They both solve the same problem: splitting code into reusable files and loading packages. But they use different syntax and have different rules.

CommonJS

CommonJS is the older module system in Node.js.

const express = require("express");

A module exports values like this:

module.exports = something;

or:

exports.myFunction = myFunction;

ES Modules

ES Modules are the standard JavaScript module system.

import express from "express";

A module exports values like this:

export default something;
export const myFunction = () => {};

Why this matters

The error happens because import expects exports in a certain shape. If a package is written as CommonJS, it may not provide a real default export in the ES Module sense.

That means these are not always equivalent:

const express = require("express");
import express from "express";

Whether import express from "express" works depends on:

  • your Node.js version
  • whether your file is treated as ESM
  • your package.json configuration
  • whether the package supports ESM directly
  • whether a transpiler like Babel or TypeScript changes the behavior

Why beginners hit this often

Many tutorials mix syntax from different environments:

  • browser JavaScript examples use import
  • older Node.js examples use require()
  • TypeScript may allow syntax that plain Node.js does not
  • bundlers sometimes make incompatible code appear to work

In real projects, you should usually pick one module system and use it consistently.

Mental Model

Think of modules like two different plug types.

  • require() is one plug type: CommonJS
  • import is another plug type: ES Modules

They both connect devices, but the shape of the plug is different.

Sometimes an adapter exists, so one can work with the other. Sometimes it does not fit cleanly, or you need extra configuration.

Another way to think about it:

  • require() says: “Load this module in the old Node.js style.”
  • import says: “Load this module in the standard JavaScript module style.”

If the module was packaged in a different style than your code expects, you get errors like:

  • has no default export
  • require is not defined
  • Cannot use import statement outside a module

So the real issue is usually not the package itself. It is a module system mismatch.

Take Quiz

Syntax and Examples

CommonJS syntax

Importing

const express = require("express");
const fs = require("fs");

Exporting

function greet(name) {
  return `Hello, ${name}`;
}

module.exports = greet;

Using it:

const greet = require("./greet");
console.log(greet("Sam"));

ES Module syntax

Default export

export default function greet(name) {
  return `Hello, ${name}`;
}

Using it:

import greet from "./greet.js";
console.log(greet("Sam"));

Named export

export function greet(name) {
  return `Hello, ${name}`;
}

export const version = "1.0";

Using it:

import { greet, version } from "./greet.js";
console.log(greet("Sam"));
console.log(version);

Why express can be confusing

In many Node.js setups, Express is commonly loaded like this:

const express = require("express");

In modern ESM-enabled Node.js projects, this often also works:

import express from "express";

But if your environment treats the package exports differently, you may need another form or a different configuration.

Make import work in Node.js

To use ESM in Node.js, one common setup is to add this to package.json:

{
  "type": "module"
}

Then your file can use:

import express from "express";

const app = express();

app.get("/", (req, res) => {
  res.send("Hello");
});

app.listen(3000);

Without ESM enabled, Node.js may expect require() instead.

Importing CommonJS from ESM

If needed, you can also import the full module namespace:

import * as expressModule from "express";

But for many CommonJS packages, this does not behave exactly like require() in the most beginner-friendly way. In normal Node.js projects, the better fix is usually to configure the project correctly rather than guessing different import forms.

Step by Step Execution

Consider this ES Module file:

import { readFile } from "fs/promises";

async function showFile() {
  const text = await readFile("message.txt", "utf8");
  console.log(text);
}

showFile();

Here is what happens step by step:

  1. Node.js sees the import statement.
  2. It checks whether the file is being treated as an ES Module.
    • This usually depends on file extension or package.json settings.
  3. Node loads fs/promises and finds the named export readFile.
  4. The showFile function is defined.
  5. showFile() is called.
  6. readFile("message.txt", "utf8") reads the file asynchronously.
  7. await pauses inside the async function until the file is read.
  8. The file contents are stored in text.
  9. console.log(text) prints the contents.

Now compare that with CommonJS:

const fs = require("fs");

fs.readFile("message.txt", "utf8", (err, text) => {
  if (err) {
    console.error(err);
    return;
  }

  console.log(text);
});

Step by step:

  1. Node.js sees require("fs").
  2. It loads the CommonJS-compatible module.
  3. The returned object is stored in fs.
  4. fs.readFile(...) starts reading the file.
  5. When reading finishes, the callback runs.
  6. If there is an error, it is logged.
  7. Otherwise, the file contents are printed.

The important difference here is not just syntax. The module loading system itself is different.

Real World Use Cases

Where require() is still used

  • Older Node.js applications
  • Legacy Express servers
  • Existing codebases that were started before ESM became common
  • Quick scripts copied from older tutorials

Example:

const mongoose = require("mongoose");
const express = require("express");

Where import is commonly used

  • Modern Node.js applications
  • Full-stack JavaScript projects
  • Frontend frameworks like React, Vue, and Svelte
  • Shared code that runs in multiple JavaScript environments

Example:

import express from "express";
import mongoose from "mongoose";

Practical situations where this matters

Building an API server

You may install Express, Mongoose, and dotenv. If your project uses ESM, all your imports should follow the ESM style consistently.

Writing utility modules

A helper file may export functions for validation, formatting, or database logic. You need matching import/export syntax across files.

Using package documentation

Some package docs show both styles. You need to choose the version that matches your project setup.

Migrating an old codebase

Teams often move from require() to import gradually. During migration, understanding interop problems is essential.

Take Quiz

Real Codebase Usage

In real projects, developers usually do one of these:

Pattern 1: Stay fully in CommonJS

This is common in older Node.js applications.

const express = require("express");
const mongoose = require("mongoose");
const dotenv = require("dotenv");

dotenv.config();

Why teams do this:

  • stable in older codebases
  • many existing files already use it
  • avoids migration work

Pattern 2: Use ES Modules consistently

This is common in newer projects.

import express from "express";
import mongoose from "mongoose";
import dotenv from "dotenv";

dotenv.config();

Why teams do this:

  • standard JavaScript syntax
  • consistent with frontend code
  • easier to share learning across environments

Common real-project patterns

Configuration loading

import dotenv from "dotenv";
dotenv.config();

Route modules

import express from "express";
const router = express.Router();

export default router;

Named utility exports

export function isValidEmail(email) {
  return email.includes("@");
}
import { isValidEmail } from "./validators.js";

Guard clauses and validation

Developers often combine imports with early-return validation patterns:

app.post("/users", (req, res) => {
  if (!req.body.email) {
    return res.status(400).json({ error: "Email is required" });
  }

  res.json({ ok: true });
});

The import system itself is separate from validation logic, but in real codebases both appear together constantly.

Common Mistakes

1. Mixing module systems without understanding the rules

Broken example:

import express from "express";
const mongoose = require("mongoose");

This can work in some setups, but it can also cause confusion or runtime errors depending on configuration.

Better approach:

  • Use one module system consistently when possible.
  • If your project is CommonJS, use require().
  • If your project is ESM, use import.

2. Forgetting to enable ES Modules in Node.js

Broken example:

import express from "express";

If your project is not configured for ESM, Node may throw an error such as:

  • Cannot use import statement outside a module

Fix:

  • Add "type": "module" to package.json, or
  • use the .mjs extension for ES module files

3. Confusing default exports and named exports

Broken example:

import { express } from "express";

If the package does not export a named express, this fails.

Another broken example:

import express from "./helpers.js";

if helpers.js contains only named exports:

export function format() {}

Fix:

  • Use import name from ... for default exports
  • Use import { name } from ... for named exports

4. Forgetting file extensions in ESM local imports

Broken example:

import { greet } from "./greet";

In Node.js ESM, local files usually need the extension:

import { greet } from "./greet.js";

5. Assuming Babel or TypeScript behavior is the same as plain Node.js

Some tools allow more flexible imports or add compatibility helpers. Code that works in one setup may fail in plain Node.js.

Avoid this by checking:

  • Is this plain Node.js?
  • Is it TypeScript?
  • Is a bundler or transpiler involved?

6. Importing CommonJS packages as if they were native ESM

Not every package exposes exports in the way you expect. If a package is CommonJS-first, require() may work more naturally unless your environment handles interop for you.

Comparisons

FeatureCommonJSES Modules
Import syntaxconst x = require("x")import x from "x"
Export syntaxmodule.exports = ... / exports.x = ...export default ... / export const x = ...
Original use in Node.jsTraditional Node.js standardNewer standard supported by Node.js
Browser supportNo native browser supportNative browser standard
Configuration in Node.jsUsually works by default in older setupsOften needs "type": "module" or .mjs
Local file importsrequire("./file")import "./file.js" usually includes extension
Default export conceptNot the same as ESM default exportBuilt in

require() vs import

SituationUsually better choice
Older Node.js projectrequire()
New Node.js project using modern syntaximport
Package docs show CommonJS examples onlyrequire() may be simpler
Frontend and backend both use modern JSimport

Default export vs named export

Export styleExample exportExample import
Default exportexport default greet;import greet from "./greet.js";
Named exportexport const greet = ...;import { greet } from "./greet.js";
Take Quiz

Cheat Sheet

Quick rules

  • require() = CommonJS
  • import = ES Modules
  • Do not assume they behave exactly the same
  • In Node.js, ESM usually needs "type": "module" in package.json or .mjs files
  • In ESM, local file imports usually include the file extension

CommonJS

const express = require("express");
module.exports = myFunction;

ES Modules

import express from "express";
export default myFunction;

Named exports

export const add = (a, b) => a + b;
import { add } from "./math.js";

Default exports

export default function add(a, b) {
  return a + b;
}
import add from "./math.js";

Common errors

  • Cannot use import statement outside a module

    • Your file is not being treated as ESM
  • has no default export

    • You are importing a default value from something that does not provide one in that form
  • require is not defined

    • You are in an ES Module environment where require() is not available by default

Good practice

  • Pick one module system per project when possible
  • Use package docs that match your setup
  • For modern Node.js apps, ESM is usually a good default
  • For older projects, CommonJS may be simpler

FAQ

Why does require() work but import fails in Node.js?

Because your project may be using CommonJS by default, while import requires ES Module support and matching export rules.

How do I enable import in Node.js?

Add this to package.json:

{
  "type": "module"
}

Then use import/export syntax consistently.

Is import better than require()?

Not automatically. import is the modern JavaScript standard, but require() is still common in older Node.js codebases.

Why does Node.js say a package has no default export?

Because you used default import syntax, but the module does not expose a default export in the way your environment expects.

Can I mix require() and import in the same project?

Sometimes, yes, but it often creates confusion. Beginners should usually choose one system and stick to it.

Should I use CommonJS or ES Modules for a new Node.js project?

For most new projects, ES Modules are a good choice if your dependencies and tooling support them well.

Do I need file extensions with import in Node.js?

For local files in Node.js ESM, usually yes. Example:

import { helper } from "./helper.js";
Take Quiz

Mini Project

Description

Build a tiny Node.js app that demonstrates both module systems by creating a reusable greeting module and loading it in an ES Module project. This helps you practice default exports, named exports, and correct Node.js configuration.

Goal

Create a small Node.js program that uses ES Modules correctly and imports functions from another file without module errors.

Requirements

  1. Create a package.json that enables ES Modules.
  2. Create one module file that exports a default function and one named value.
  3. Create an entry file that imports both exports and prints output.
  4. Use valid Node.js ESM syntax, including the local file extension.
  5. Run the program successfully without using require().
Take Quiz

Keep learning

Related questions

View more