Skip to main content

tsconfig-paths vs module-alias vs pathsify: TypeScript Path Aliases (2026)

·PkgPulse Team

TL;DR

TypeScript path aliases let you write import { db } from "@/lib/db" instead of import { db } from "../../../../lib/db". TypeScript's compiler understands these paths via tsconfig.json paths config — but the runtime doesn't. After compilation, Node.js still sees @/lib/db and crashes. tsconfig-paths hooks into Node's module resolution at startup to fix this. module-alias is a older alternative using package.json aliases. In 2026: for most projects, just use a bundler (tsx, esbuild, Vite, Next.js) which handles path aliases automatically — tsconfig-paths for production Node.js servers.

Key Takeaways

  • tsconfig-paths: ~8M weekly downloads — reads tsconfig.json paths at runtime, register before app code
  • module-alias: ~2M weekly downloads — maps aliases in package.json _moduleAliases, no tsconfig dependency
  • pathsify: minimal downloads — ESM-native tsconfig paths resolution
  • The problem: TypeScript compiles @/lib/db@/lib/db in JS (doesn't rewrite paths)
  • Best solution for most projects: use tsx, Vite, esbuild, or Next.js — they handle this transparently
  • For production Node.js servers: tsconfig-paths or tsc-alias (rewrites paths at compile time)

The Problem

// tsconfig.json:
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@lib/*": ["src/lib/*"],
      "@types/*": ["src/types/*"]
    }
  }
}

// Your TypeScript:
import { db } from "@/lib/database"      // ✅ TypeScript understands this
import { Package } from "@types/package"  // ✅

// After tsc compilation (dist/index.js):
const database_1 = require("@/lib/database")  // ❌ Node.js: "Cannot find module '@/lib/database'"

// The path alias was NOT rewritten — Node.js has no idea what @/ means

Solutions Overview

Option 1: tsconfig-paths       — runtime hook, reads paths from tsconfig at startup
Option 2: module-alias         — runtime hook, reads aliases from package.json
Option 3: tsc-alias            — compile-time, rewrites paths in compiled JS files
Option 4: bundler (esbuild/Vite) — handles paths during bundling (no runtime cost)
Option 5: tsx / ts-node        — transpiles on-the-fly, handles paths automatically

tsconfig-paths

tsconfig-paths — runtime path resolution:

Setup

npm install tsconfig-paths

Node.js usage

# Register before your app starts:
node -r tsconfig-paths/register dist/index.js

# Or in package.json:
{
  "scripts": {
    "start": "node -r tsconfig-paths/register dist/index.js",
    "dev": "tsx src/index.ts"  # tsx handles paths automatically
  }
}

Programmatic registration

// src/register.ts — import this FIRST in your entry point:
import { register } from "tsconfig-paths"
import { resolve } from "node:path"

// Register paths from tsconfig.json:
register({
  baseUrl: resolve(__dirname, ".."),  // Adjust to your project root
  paths: {
    "@/*": ["src/*"],
    "@lib/*": ["src/lib/*"],
  },
})

// Now @/ imports work in subsequent requires/imports
// src/index.ts:
import "./register"  // Must be FIRST import
import { db } from "@/lib/database"  // Now works at runtime
import { PackageService } from "@lib/services/package"

With ts-node

# ts-node with tsconfig-paths:
ts-node -r tsconfig-paths/register src/index.ts

# Or via tsconfig.json (ts-node section):
# {
#   "ts-node": {
#     "require": ["tsconfig-paths/register"]
#   }
# }

GitHub Actions / Docker

# Dockerfile:
CMD ["node", "-r", "tsconfig-paths/register", "dist/index.js"]
# GitHub Actions:
- name: Start server
  run: node -r tsconfig-paths/register dist/index.js &

Full tsconfig.json example

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "outDir": "dist",
    "rootDir": "src",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@lib/*": ["src/lib/*"],
      "@services/*": ["src/services/*"],
      "@models/*": ["src/models/*"],
      "@utils/*": ["src/utils/*"],
      "@config": ["src/config/index.ts"]
    },
    "strict": true,
    "esModuleInterop": true
  }
}

module-alias

module-alias — package.json-based aliases:

Setup

npm install module-alias

Configuration (package.json)

{
  "name": "pkgpulse-api",
  "_moduleAliases": {
    "@": "./dist",
    "@lib": "./dist/lib",
    "@services": "./dist/services",
    "@models": "./dist/models",
    "@utils": "./dist/utils",
    "@config": "./dist/config/index.js"
  }
}
// src/index.ts — register at the top:
import "module-alias/register"

// Now aliases work:
import { db } from "@lib/database"
import { PackageService } from "@services/package"

Key differences from tsconfig-paths

tsconfig-paths:
  - Reads paths directly from tsconfig.json
  - Paths are relative to source (src/) — matches TypeScript resolution
  - Must point to dist/ in production (or adjust baseUrl)

module-alias:
  - Reads from package.json _moduleAliases
  - Must point to compiled dist/ files (production-ready)
  - Separate config from tsconfig — can get out of sync
  - More explicit about runtime vs compile-time paths

tsc-alias (compile-time alternative)

tsc-alias — rewrites paths in compiled output:

npm install -D tsc-alias
// package.json:
{
  "scripts": {
    "build": "tsc && tsc-alias",
    "start": "node dist/index.js"   // No -r flag needed!
  }
}
tsc:       src/index.ts → dist/index.js (paths NOT rewritten)
tsc-alias: dist/index.js → dist/index.js (paths rewritten to relative)

Result in dist/index.js:
  Before: const database = require("@/lib/database")
  After:  const database = require("./lib/database")  ✅ Node.js works natively

The Better Alternative: Just Use a Bundler/Transpiler

For most projects, you don't need tsconfig-paths or module-alias at all:

tsx / ts-node-esm:
  → transpiles TypeScript on-the-fly, handles paths from tsconfig automatically
  → use for scripts, CLI tools, development

Vite (+ SvelteKit, Astro):
  → configure resolve.alias in vite.config.ts
  → built-in path alias support, no extra package needed

Next.js:
  → automatically handles tsconfig paths
  → just configure tsconfig.json, Next.js handles the rest

esbuild (via tsup/tsdown):
  → configure tsconfigRaw.compilerOptions.paths
  → bundled output has no path aliases at all (inlined)

Vite path aliases

// vite.config.ts:
import { defineConfig } from "vite"
import { resolve } from "node:path"

export default defineConfig({
  resolve: {
    alias: {
      "@": resolve(__dirname, "./src"),
      "@lib": resolve(__dirname, "./src/lib"),
      "@components": resolve(__dirname, "./src/components"),
    },
  },
})

// tsconfig.json (for TypeScript type checking):
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"],
      "@lib/*": ["./src/lib/*"]
    }
  }
}

Next.js (automatic)

// tsconfig.json — Next.js reads and handles this automatically:
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
// No tsconfig-paths, no module-alias needed — Next.js handles it

Feature Comparison

Featuretsconfig-pathsmodule-aliastsc-alias
When it runsRuntimeRuntimeCompile time
Config sourcetsconfig.jsonpackage.jsontsconfig.json
ESM support⚠️ Partial⚠️ Partial
Production overheadTiny (startup only)Tiny (startup only)None
Keeps paths in sync✅ (reads tsconfig)❌ (manual sync)✅ (reads tsconfig)
Works without bundler
Weekly downloads~8M~2M~500K

When to Use Each

Use tsconfig-paths if:

  • Production Node.js server compiled with tsc (Express, Fastify, NestJS)
  • Want paths to stay in sync with tsconfig.json automatically
  • CommonJS output ("module": "commonjs" in tsconfig)

Use module-alias if:

  • Need explicit control over runtime vs compile-time paths
  • Monorepo where paths differ between packages
  • Non-TypeScript projects that want path aliases

Use tsc-alias if:

  • Want zero runtime overhead — paths rewritten at compile time
  • ESM output ("module": "es2022" or "nodenext")
  • CI/CD where you want no runtime dependencies

Don't need any of these if:

  • Using Next.js, Nuxt, SvelteKit — framework handles paths
  • Using Vite — configure resolve.alias in vite.config.ts
  • Using tsx for scripts — reads tsconfig automatically
  • Using tsup/tsdown — bundled output has no aliases

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on tsconfig-paths v4.x, module-alias v2.x, and tsc-alias v1.x.

Compare TypeScript tooling and build packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.