Fix: Webpack/Vite Path Alias Not Working — Module Not Found with @/ Prefix
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:
- webpack —
resolve.aliasinwebpack.config.js - Vite —
resolve.aliasinvite.config.ts - TypeScript —
pathsintsconfig.json(for type checking only — doesn’t affect bundling) - Jest/Vitest —
moduleNameMapper(separate from webpack/Vite config) - Babel —
babel-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 errorCreate 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
pathsonly 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 setpaths, 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 importFix 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.extraNodeModulesEnd-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 everywhereStill 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Vite Proxy Not Working — API Requests Not Forwarded or 404/502 Errors
How to fix Vite dev server proxy issues — proxy configuration in vite.config.ts, path rewriting, WebSocket proxying, HTTPS targets, and common misconfigurations.
Fix: Vite Environment Variables Not Working
How to fix Vite environment variables showing as undefined — missing VITE_ prefix, wrong .env file for the mode, import.meta.env vs process.env, TypeScript types, and SSR differences.
Fix: CodeMirror Not Working — Editor Not Rendering, Extensions Not Loading, or React State Out of Sync
How to fix CodeMirror 6 issues — basic setup, language and theme extensions, React integration, vim mode, collaborative editing, custom keybindings, and read-only mode.
Fix: GSAP Not Working — Animations Not Playing, ScrollTrigger Not Firing, or React Cleanup Issues
How to fix GSAP animation issues — timeline and tween basics, ScrollTrigger setup, React useGSAP hook, cleanup and context, SplitText, stagger animations, and Next.js integration.