✨ Introduction
When working on a Node.js project with TypeScript, you've probably found yourself writing import statements like this:
As your project grows, you end up with deeper folder structures — and soon you're drowning in ../../../../
imports. These long relative paths are hard to read, easy to break when changing files, and frankly, not fun to work with.
That's where TypeScript path aliases come to the rescue.
Path aliases let you define clean import paths (like @/utils
) that map to specific folders or files in your project. Instead of those messy relative paths, you can write:
This transformation brings immediate benefits:
- 📖 Readability: Your imports become self-documenting and easier to scan
- 🔧 Maintainability: Restructuring folders won't cascade into import hell
- ⚡ Developer Experience: IDEs can better track dependencies and auto-update imports
In this guide, I'll walk you through setting up path aliases in a Node.js + TypeScript project and make sure everything works fine including:
- 🐢 Production builds with native
node
- 🧑💻 Local development with
ts-node-dev
ornodemon
- 🧪 Testing with
jest
You can follow along with the complete example code on GitHub.
Ready to clean up those imports? Let's dive in! 🚀
📖 Our Approach: Single Source of Truth
There are many ways to set up TypeScript path aliases in Node.js, but some options are more practical than others.
Many tutorials will have you duplicating the same path mappings across multiple config files:
- Once in
tsconfig.json
for TypeScript compilation - Again in
jest.config.js
for testing - Yet again in your bundler config (Webpack, Rollup, etc.)
- And sometimes even in development tools like
ts-node-dev
ornodemon
Having the same configuration in many places makes everything harder to maintain. Change a path? Now you need to remember to update it in 3-4 different places. Miss one? Your build breaks in mysterious ways.
We're taking a better approach.. In this guide, we'll configure tsconfig.json
as our single source of truth and make every other tool read from it. This means:
- ✨ One place to define paths - All aliases live in
tsconfig.json
- ✨ Automatic synchronization - Tools pull config from the same source
- ✨ Zero duplication - Change once, works everywhere
- ✨ Fewer bugs - No more "it works in dev but breaks in tests"
💡 Philosophy: Good tooling should work together, not against each other.
🛠️ Define Path Aliases
All path alias magic starts in your tsconfig.json
file — this is where TypeScript learns how to resolve your custom import paths during compilation.
We need to configure two essential properties:
baseUrl
: Sets the root directory for module resolution. We need it to establish the starting point for all path mappings.paths
: Maps alias patterns to actual folder locations. This is the core of our alias system.
In the paths
configuration, you can add as many aliases as you need, and they can point to any folder structure in your project.
With this configuration, you can transform messy imports:
💡 Tip: Although most examples use aliases that point to folders (like
"@/src/*"
), you can also alias individual files. This is especially useful when working with internal packages where you only want to expose a single entry point, like anindex.ts
file.For example:
With this setup:
- ✅ Consumers can import cleanly:
import { something } from "@internal/utils";
- 🚫 But they can’t import from deeper internals like:
import { hiddenThing } from "@internal/utils/private/stuff";
(unless you explicitly create a new alias for it)This helps enforce boundaries and keep your internal APIs clean.
Making Aliases Work with node
Here's where things get tricky. TypeScript knows about our aliases, but Node.js doesn't.
When you compile your TypeScript code, the aliases remain in the JavaScript output:
Result: 💥 Runtime error
This happens because Node has no knowledge of the TypeScript paths configuration. Once the code is compiled, it still contains import paths like @/src/server
, which Node cannot resolve.
To fix this, we need a tool that rewrites the import paths in the emitted .js
files after compilation. In our case, we are going to use tsc-alias.
Install it as a dev dependency
Then, update your build script to run tsc-alias
right after compiling with tsc
:
This way, tsc-alias
will post-process the compiled files inside dist/
, replacing any alias-based imports with the corresponding relative paths that Node can understand.
Now, if you run the build command again and start the server, it should work without any issues.
👨💻 Path Aliases for Local Development
We've solved the production build puzzle, but development has its own challenge. Development tools like ts-node-dev
and nodemon
run TypeScript files directly without compilation — and they don't understand our aliases either.
If we try running the development server now, we get an error:
To fix this, we need something that can resolve at runtime the path aliases we have defined. In our case, we are going to use a library called tsconfig-paths which reads the tsconfig.json
file and sets up the path aliases automatically when running TypeScript files.
Let's install it as a dev dependency:
Then, update your package.json
scripts to use ts-node-dev
or nodemon
with tsconfig-paths
:
Making Path Aliases Work with jest
By default, Jest does not read the paths from tsconfig.json
, but if we're using ts-jest
, we can bridge this gap using the tools it provides.
There are two key options we need to set:
modulePaths
: This option tells Jest where to start resolving non-relative importsmoduleNameMapper
: This maps alias patterns (like@/src/*
) to real paths on disk.
To generate the moduleNameMapper
from your tsconfig.json
, we can use the helper function pathsToModuleNameMapper
provided by ts-jest
. It converts the paths config into something Jest can understand.
Here’s how your Jest config should look after setting it up properly:
🎯 Conclusion
In this post, we’ve explored what TypeScript path aliases are and how to configure them properly in a Node.js environment — centralizing everything in a single place (tsconfig.json
) and making sure that tools like node
, ts-node-dev
, nodemon
, and jest
can all understand them.
I hope you found this post helpful and that it saves you time when structuring your own projects.
Thanks for reading! 🙌