Skip to content

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

FixDevs ·

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 — each tool has its own alias resolution:

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

The most common mistake: configuring aliases in only one place. TypeScript’s tsconfig.json paths don’t affect webpack or Vite bundling. Webpack’s resolve.alias doesn’t affect TypeScript’s type checker. Each tool needs its own configuration.

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 don’t 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 doesn’t 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: {
    '^@/(.*)$': path.resolve(__dirname, 'src/$1'),
    // Or use <rootDir> which Jest replaces with the project root
    '^@/(.*)$': '<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’re 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/*"]
    }
  }
}

For related build issues, see Fix: Vite Failed to Resolve Import and Fix: Webpack Module Not Found.

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