Skip to content

Fix: Webpack/Vite Path Alias Not Working — Module Not Found with @/ Prefix

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix path alias errors in webpack and Vite — configuring resolve.alias, tsconfig paths, babel-plugin-module-resolver, Vite alias configuration, and Jest moduleNameMapper.

The Error

A path alias like @/ or ~/ fails to resolve:

Module not found: Error: Can't resolve '@/components/Button'
in '/home/user/project/src/pages'

ERROR in ./src/pages/Home.tsx
Module not found: Error: Can't resolve '@/utils/api'

Or TypeScript shows a type error but the build works (or vice versa):

Cannot find module '@/components/Button' or its corresponding type declarations.
ts(2307)

Or Vite resolves the alias correctly but Jest tests fail:

Cannot find module '@/utils/helpers' from 'src/services/api.test.ts'

Why This Happens

Path aliases require configuration in multiple tools, and each tool resolves imports independently. This is the single most important thing to understand: webpack’s resolve.alias, TypeScript’s paths, and Jest’s moduleNameMapper are completely separate systems. Configuring one does not affect the others. A working IDE (TypeScript) does not mean a working build (webpack), and a working build does not mean working tests (Jest).

The confusion comes from the fact that all three configurations look similar — they all map @/ to src/ — but they operate at different stages of the pipeline. TypeScript’s paths only affects type checking. Webpack’s resolve.alias only affects bundling. Jest’s moduleNameMapper only affects test execution. If you configure aliases in tsconfig.json alone, your IDE stops showing errors, which makes you think the problem is fixed — until the build fails.

Here are the tools and their configuration points:

  • webpackresolve.alias in webpack.config.js
  • Viteresolve.alias in vite.config.ts
  • TypeScriptpaths in tsconfig.json (for type checking only — does not affect bundling)
  • Jest/VitestmoduleNameMapper (separate from webpack/Vite config)
  • Babelbabel-plugin-module-resolver (for non-webpack environments)

Diagnostic Timeline

Here is how an experienced engineer debugs a path alias failure systematically.

Minute 0 — Classify the error. The first question is: where does the error appear? There are three distinct failure points. A build error (webpack/Vite output) means the bundler cannot resolve the alias. A TypeScript error (red squiggle in IDE, tsc --noEmit failure) means tsconfig.json paths are misconfigured. A test error (Jest/Vitest output) means the test runner’s module resolution is not configured. Each has a different fix.

Minute 1 — First instinct: “fix tsconfig paths.” This is the most common wrong first step when the build fails. Developers see the IDE error disappear after adding paths to tsconfig.json and assume the build is fixed too. But TypeScript paths do not affect webpack or Vite at all. If the error is a build error, you need to configure resolve.alias in the bundler config. If it is only a TypeScript error, then tsconfig.json is the right place.

Minute 2 — Check whether the project uses webpack or Vite. Look for webpack.config.js, webpack.config.ts, vite.config.ts, or vite.config.js in the project root. If the project uses Create React App (CRA), webpack config is hidden inside react-scripts and requires craco or react-app-rewired to customize. If the project uses Next.js, aliases can be configured in next.config.js or directly via tsconfig.json paths (Next.js reads them automatically).

Minute 3 — Verify baseUrl in tsconfig.json. TypeScript paths require baseUrl to be set. If baseUrl is missing, paths entries are silently ignored. Set "baseUrl": "." to make paths relative to the project root. Without this, "@/*": ["src/*"] resolves to nothing.

Minute 4 — Check for multiple tsconfig files. Modern projects often have tsconfig.json, tsconfig.app.json, tsconfig.node.json, and tsconfig.build.json. The IDE typically uses the root tsconfig.json, but the build may use tsconfig.app.json. If paths is in the root but not in the file the build references, the build fails while the IDE looks fine.

Minute 5 — Verify the bundler config independently. Run the build and check the error. If it says “Module not found” with the @/ prefix, the bundler config is missing or wrong. Open webpack.config.js or vite.config.ts and confirm resolve.alias maps @ to the correct absolute path using path.resolve(__dirname, 'src'). A relative path like './src' can break depending on the working directory.

Minute 6 — Run tests separately. If the build passes but tests fail with “Cannot find module ’@/…’”, Jest or Vitest needs its own configuration. Jest does not read webpack or Vite config. Add moduleNameMapper in jest.config.js or verify that Vitest inherits aliases from vite.config.ts.

Minute 7 — Check for path.resolve vs string values. A common cause of silent failure in webpack is using a relative string './src' instead of path.resolve(__dirname, 'src') for the alias value. Relative strings resolve relative to the process working directory, which can differ depending on how webpack is invoked (from the project root vs. from a monorepo root vs. from an npm script). Always use path.resolve with __dirname for deterministic resolution.

Minute 8 — Verify the extensions list. If your alias points to a directory but the import does not include a file extension (import { Button } from '@/components/Button'), webpack needs resolve.extensions to include .ts, .tsx, .js, and .jsx. Without this, webpack may find the directory but fail to resolve the file inside it.

Fix 1: Configure Aliases in webpack

// webpack.config.js
const path = require('path');

module.exports = {
  resolve: {
    alias: {
      // Map '@' to the src directory
      '@': path.resolve(__dirname, 'src'),

      // Multiple aliases
      '@components': path.resolve(__dirname, 'src/components'),
      '@utils': path.resolve(__dirname, 'src/utils'),
      '@hooks': path.resolve(__dirname, 'src/hooks'),
      '@assets': path.resolve(__dirname, 'src/assets'),
      '~': path.resolve(__dirname, 'src'),  // Alternative alias
    },
    // Required for aliases to work with TypeScript files
    extensions: ['.ts', '.tsx', '.js', '.jsx'],
  },
};

Verify with webpack --display-modules:

npx webpack --display-modules 2>&1 | grep "@/"
# Should show the resolved path, not an error

Create React App (CRA) — alias configuration requires ejecting or using craco/react-app-rewired:

// craco.config.js
const path = require('path');

module.exports = {
  webpack: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },
};
# Replace react-scripts with craco in package.json scripts
"start": "craco start",
"build": "craco build",
"test": "craco test",

Fix 2: Configure Aliases in Vite

// vite.config.ts
import { defineConfig } from 'vite';
import path from 'path';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@components': path.resolve(__dirname, './src/components'),
      '@utils': path.resolve(__dirname, './src/utils'),
      '@hooks': path.resolve(__dirname, './src/hooks'),
    },
  },
});

Using fileURLToPath for ESM compatibility (when __dirname is unavailable):

// vite.config.ts — ESM style
import { defineConfig } from 'vite';
import { fileURLToPath, URL } from 'node:url';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
});

Nuxt 3 — aliases are built in (@ and ~ both map to the project root by default):

// nuxt.config.ts — extend default aliases
export default defineNuxtConfig({
  alias: {
    '@utils': '/<rootDir>/utils',
    '@stores': '/<rootDir>/stores',
  },
});

Fix 3: Configure TypeScript paths

TypeScript’s paths option maps aliases for the type checker. This is separate from bundler configuration but must match:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",          // Required for paths to work — usually "." (project root)
    "paths": {
      "@/*": ["src/*"],      // "@/foo" → "src/foo"
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"],
      "@hooks/*": ["src/hooks/*"],
      "~/*": ["src/*"]
    }
  },
  "include": ["src"]
}

Important: TypeScript paths only affect type checking — they do not transpile or bundle anything. You must also configure the equivalent alias in your bundler (webpack/Vite). If you only set paths, TypeScript stops showing errors but the build still fails.

Verify TypeScript resolves the path:

npx tsc --noEmit --traceResolution 2>&1 | grep "@/components"
# Shows how TypeScript resolves each import

Fix 4: Configure Jest moduleNameMapper

Jest has its own module resolution system and does not read webpack or Vite configs:

// jest.config.json (or jest.config.js)
{
  "moduleNameMapper": {
    "^@/(.*)$": "<rootDir>/src/$1",
    "^@components/(.*)$": "<rootDir>/src/components/$1",
    "^@utils/(.*)$": "<rootDir>/src/utils/$1"
  }
}
// jest.config.js
const path = require('path');

module.exports = {
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  // Also configure module directories
  modulePaths: ['<rootDir>/src'],
};

Auto-generate Jest config from tsconfig.json paths using ts-jest:

// jest.config.js — uses tsconfig paths automatically
const { pathsToModuleNameMapper } = require('ts-jest');
const { compilerOptions } = require('./tsconfig.json');

module.exports = {
  preset: 'ts-jest',
  moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
    prefix: '<rootDir>/',
  }),
};

This keeps Jest config in sync with TypeScript config — change tsconfig.json paths and Jest updates automatically.

Fix 5: Configure Vitest Alias

Vitest can share Vite’s alias configuration, but requires explicit setup:

// vite.config.ts — shared config for both Vite and Vitest
import { defineConfig } from 'vite';
import { fileURLToPath, URL } from 'node:url';

export default defineConfig({
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  test: {
    // Vitest uses the same alias from resolve.alias above
    environment: 'jsdom',
  },
});

Separate Vitest config that extends Vite config:

// vitest.config.ts
import { defineConfig, mergeConfig } from 'vitest/config';
import viteConfig from './vite.config';

export default mergeConfig(viteConfig, defineConfig({
  test: {
    environment: 'jsdom',
    // Alias is inherited from viteConfig
  },
}));

Fix 6: Use babel-plugin-module-resolver for Babel Projects

For non-webpack projects using Babel directly (Expo, React Native, Jest with Babel):

npm install --save-dev babel-plugin-module-resolver
// .babelrc or babel.config.json
{
  "plugins": [
    ["module-resolver", {
      "root": ["./src"],
      "alias": {
        "@": "./src",
        "@components": "./src/components",
        "@utils": "./src/utils",
        "@assets": "./src/assets"
      },
      "extensions": [".js", ".jsx", ".ts", ".tsx"]
    }]
  ]
}

React Native — required for Expo and bare React Native (Metro bundler also needs config):

// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');

const config = getDefaultConfig(__dirname);

config.resolver.extraNodeModules = {
  '@': path.resolve(__dirname, 'src'),
};

module.exports = config;

Fix 7: Verify the Full Configuration Chain

A quick checklist to ensure all tools are configured:

Tool                  Config file              Key setting
─────────────────────────────────────────────────────────
webpack               webpack.config.js        resolve.alias
Vite                  vite.config.ts           resolve.alias
TypeScript            tsconfig.json            compilerOptions.paths + baseUrl
Jest                  jest.config.js           moduleNameMapper
Vitest                vite.config.ts           resolve.alias (shared with Vite)
Babel (standalone)    .babelrc                 babel-plugin-module-resolver
React Native (Metro)  metro.config.js          resolver.extraNodeModules

End-to-end verification script:

# 1. TypeScript type check (catches tsconfig.json path issues)
npx tsc --noEmit

# 2. Build (catches webpack/Vite alias issues)
npm run build

# 3. Tests (catches Jest/Vitest alias issues)
npm test

# All three must pass for aliases to work everywhere

Still Not Working?

Case sensitivity — file systems on macOS and Windows are case-insensitive, but Linux (CI/CD, Docker) is case-sensitive. @/components/button and @/components/Button are different files on Linux:

# Check actual file names
ls src/components/
# If file is Button.tsx, import as '@/components/Button' — not '@/components/button'

@ in CSS — if you are using @ aliases in CSS/SCSS imports, you may need additional configuration. Vite handles this with the same resolve.alias. webpack needs css-loader with modules: true or the resolve config to apply to CSS.

Alias resolution in tsconfig.json extends — if your project uses multiple tsconfig files (tsconfig.json, tsconfig.app.json, tsconfig.node.json), ensure paths is in the right file:

// tsconfig.app.json (the one used for type checking src/)
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

Trailing wildcard mismatch. A common typo is writing "@": ["src"] instead of "@/*": ["src/*"]. Without the /* wildcard, TypeScript can resolve import '@' but not import '@/components/Button'. Always use the wildcard form for directory aliases:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["src/*"],
      "@": ["src/index"]
    }
  }
}

Next.js handles aliases automatically from tsconfig.json. If you are using Next.js, you do not need to configure resolve.alias in next.config.js. Next.js reads paths from tsconfig.json and configures webpack internally. Adding a separate webpack alias for the same prefix can cause conflicts.

Storybook requires separate alias configuration. Storybook uses its own webpack/Vite config. Add aliases in .storybook/main.js:

// .storybook/main.js
const path = require('path');

module.exports = {
  webpackFinal: async (config) => {
    config.resolve.alias = {
      ...config.resolve.alias,
      '@': path.resolve(__dirname, '../src'),
    };
    return config;
  },
};

ESLint import resolver needs separate config. If ESLint shows “Unable to resolve path to module ’@/…’” but the build works, ESLint’s import plugin does not read webpack or Vite aliases. Install eslint-import-resolver-alias or eslint-import-resolver-typescript:

npm install --save-dev eslint-import-resolver-typescript
// .eslintrc.json
{
  "settings": {
    "import/resolver": {
      "typescript": {
        "project": "./tsconfig.json"
      }
    }
  }
}

Monorepo alias collisions. In monorepos using Turborepo, Nx, or yarn workspaces, multiple packages may define the same @/ alias. If package A imports @/utils and package B also has a @/utils alias, the wrong one may resolve depending on which tsconfig.json is picked up. Use package-specific prefixes (@app/, @shared/) or verify that each package’s build uses its own config file.

For related build issues, see Fix: TypeScript Path Alias Not Working, Fix: Vite Failed to Resolve Import, Fix: Webpack Module Not Found, and Fix: Jest Cannot Find Module.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles