How to Fix: Cannot use ‘import.meta’ outside a module when using next.config.ts
The Cannot use ‘import.meta’ outside a module error in next.config.ts happens because the config file is being evaluated in a runtime path that expects CommonJS semantics, while your TypeScript config contains ESM-only syntax. In practice, import.meta is valid only when the file is executed as a real ES module, and that assumption does not always hold during Next.js config loading.
Understanding the Root Cause
This bug appears when a project uses next.config.ts and accesses import.meta, often for patterns like resolving local paths, detecting the current file location, or composing URLs with new URL(…, import.meta.url).
The problem is that Next.js configuration loading is not the same as loading application source files. Your app code may be bundled and transpiled with modern ES module support, but the config file is processed earlier by Node.js and Next’s config loader. Depending on the version, environment, and loader path, next.config.ts may not run as a native ESM module even if the syntax looks modern.
That leads to a mismatch:
- import.meta requires an ES module execution context.
- next.config.ts may be transpiled or executed in a way that behaves like CommonJS.
- When Node encounters import.meta outside true ESM execution, it throws the error.
In short, this is not usually a TypeScript typing problem. It is a module system mismatch between ESM-only config syntax and the way Next.js currently evaluates the config file.
Step-by-Step Solution
The most reliable fix is to avoid using import.meta inside next.config.ts. Replace it with APIs that work in the config runtime, such as __dirname, path.resolve, or simply relative values that Next can consume directly.
If you are using import.meta.url to build file paths, switch to a JavaScript config file that matches the runtime expectation.
Recommended fix: rename the config file to CommonJS or plain JavaScript syntax.
Option 1: Use next.config.js with safe Node-compatible path handling.
const path = require('path')/** @type {import('next').NextConfig} */const nextConfig = { webpack(config) { config.resolve.alias['@root'] = path.resolve(__dirname) return config },}module.exports = nextConfig
If your original TypeScript config looked like this:
const nextConfig = { experimental: { someFeature: true, }, webpack(config) { // Problematic in next.config.ts // new URL('./src', import.meta.url) return config },}export default nextConfig
Replace the path logic with path.resolve:
const path = require('path')/** @type {import('next').NextConfig} */const nextConfig = { experimental: { someFeature: true, }, webpack(config) { config.resolve.alias['@src'] = path.resolve(__dirname, 'src') return config },}module.exports = nextConfig
Option 2: If you want to keep TypeScript, remove import.meta entirely from next.config.ts and stick to values that do not depend on ESM-only runtime behavior.
import path from 'path'import type { NextConfig } from 'next'const nextConfig: NextConfig = { webpack(config) { config.resolve.alias = { ...config.resolve.alias, '@src': path.resolve(process.cwd(), 'src'), } return config },}export default nextConfig
This works because process.cwd() and path.resolve() do not require import.meta.
Best practice: use process.cwd() for project-root paths and reserve import.meta for runtime environments that are guaranteed to be native ESM.
If your use case was specifically this pattern:
const fileUrl = new URL('./some-file', import.meta.url)
Convert it to:
const path = require('path')const filePath = path.resolve(__dirname, 'some-file')
After updating the config:
- Stop the dev server.
- Rename or edit the config file.
- Remove import.meta usage from the config.
- Restart with next dev.
npm run dev
If the issue was caused only by config parsing, the startup error should disappear immediately.
Common Edge Cases
- package.json uses “type”: “module”: This can change how Node interprets files, but it does not guarantee that next.config.ts will behave exactly like native ESM during Next config loading.
- Mixing require and import: If you partially migrate the config and leave incompatible syntax behind, you may trade one module error for another.
- Custom webpack aliases: Many developers use import.meta.url only for alias resolution. In config files, path.resolve(__dirname, …) or process.cwd() is usually safer.
- Monorepo setups: In a workspace, process.cwd() may point to the app package rather than the monorepo root. If you need the workspace root, compute it explicitly with path.resolve().
- ESM examples copied from Vite: Vite config commonly uses import.meta. That does not automatically translate to Next.js config, because the config loaders are different.
- Version-specific behavior: Experimental config support can evolve between Next.js versions. Even if a pattern works in one release or canary build, using Node-compatible path APIs is still the safest approach.
FAQ
Can I use import.meta anywhere in a Next.js project?
Yes, but only in places that are actually compiled and executed as ES modules. The issue here is specific to next.config.ts, which is part of the framework configuration loading path rather than normal application bundling.
Why does import.meta work in other tools like Vite but fail here?
Because tooling runtimes differ. Vite’s config pipeline is designed around ESM-friendly patterns, while Next.js config evaluation may still pass through a path where CommonJS-compatible execution is expected.
Should I switch from next.config.ts to next.config.js?
If you need maximum compatibility today, yes. A plain next.config.js or a config that avoids import.meta is the most stable fix for this specific startup error.
The key takeaway is simple: do not rely on import.meta inside next.config.ts unless the config loader explicitly guarantees native ESM execution. For path resolution and config composition, use path.resolve, __dirname, and process.cwd() instead.