node.jstypescriptpathshared-libraries

Node.js TypeScript ES Modules: "SyntaxError: The requested module does not provide an export named" when using path aliases


I'm developing a backend application using Node.js, TypeScript and Hono. I'm trying to use path aliases defined in my tsconfig.json to import modules, but I'm facing the following error now:

Error Message:

file:///D:/workspace/../backend/src/controllers/MRFController.ts:1
...
SyntaxError: The requested module '@shared/validations/claimValidationSchema' does not provide an export named 'claimsArrayValidationSchema'
    at ModuleJob._instantiate (node:internal/modules/esm/module_job:131:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:213:5)
    at async ModuleLoader.import (node:internal/modules/esm/loader:316:24)
    at async loadESM (node:internal/process/esm_loader:34:7)
    at async handleMainPromise (node:internal/modules/run_main:66:12)

I'm sharing my project structure and some codebase you might need to review.

project-root/
├── backend/
│   ├── package.json
│   ├── tsconfig.json
│   ├── src/
│   │   ├── controllers/
│   │   │   └── MRFController.ts
│   │   ├── index.ts
│   │   └── ... (other backend source files)
│   └── ... (other backend files)
├── frontend/
│   ├── package.json
│   ├── tsconfig.json
│   ├── vite.config.ts
│   ├── src/
│   │   └── ... (frontend source files)
│   └── ... (other frontend files)
├── shared/
│   ├── package.json (optional)
│   ├── tsconfig.json (optional)
│   ├── src/
│   │   └── validations/
│   │       └── claimValidationSchema.ts
│   │   └── types/
│   │       └── ... (shared TypeScript types)
│   └── ... (other shared files)
└── ... (other project files)

MRFController.ts:

import { promises as fs } from "fs";
import path from "path";
import { fileURLToPath } from "url";
import type { Context } from "hono";
import { convertClaimsToMRF } from "@/services/mrfService";
import { ENTITY_NAME, MESSAGES } from "@/utils/constants";
import { getCurrentTimestamp, parseTimestampString } from "@/utils/helpers";
import { MRFReportStatus } from "@/utils/enums";
import type { MRFResponse } from "@shared/types/apiTypes";
import type { MRFRow } from "@shared/types/mrfTypes";
import { claimsArrayValidationSchema } from "@shared/validations/claimValidationSchema";

// Convert `import.meta.url` to a file path
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const jsonFilesDir = path.join(__dirname, "..", "..", "..", "data", "files");

export default class MRFController {
  static async generateMRF(context: Context) {
    try {
      const { claims } = await context.req.json();

      try {
        claimsArrayValidationSchema.parse(claims);
      } catch (error) {
        console.log(error);
        return context.json<MRFResponse>(
          {
            success: false,
            message: MESSAGES.VALIDATION_ERROR,
            error: JSON.stringify(error),
          },
          400
        );
      }

      // ... rest of the code
    } catch (error) {
      // ... error handling
    }
  }

  // ... other methods
}

claimValidationSchema.ts:

import { z } from "zod";

const claimValidationSchema = z.object({
  claimId: z.string(),
  subscriberId: z.string(),
  memberSequence: z.number(),
  claimStatus: z.string(),
  billed: z.number(),
  allowed: z.number(),
  paid: z.number(),
  paymentStatusDate: z.string(),
  serviceDate: z.string(),
  receivedDate: z.string(),
  entryDate: z.string(),
  processedDate: z.string(),
  paidDate: z.string(),
  paymentStatus: z.string(),
  groupName: z.string(),
  groupId: z.string(),
  divisionName: z.string(),
  divisionId: z.string(),
  plan: z.string(),
  planId: z.string(),
  placeOfService: z.string(),
  claimType: z.string(),
  procedureCode: z.string(),
  memberGender: z.string(),
  providerId: z.string(),
  providerName: z.string(),
});

export const claimsArrayValidationSchema = z.array(claimValidationSchema);

Backend tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "allowJs": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",
    /* Linting */
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    /* Paths */
    "baseUrl": ".",
    "paths": {
      "~/*": ["src/*"],
      "@shared/*": ["../shared/src/*"]
    }
  },
  "include": ["src", "vite.config.ts"]
}

Frontend tsconfig.json (working fine):

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "allowJs": true,
    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",
    /* Linting */
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    /* Paths */
    "baseUrl": ".",
    "paths": {
      "~/*": ["src/*"],
      "@shared/*": ["../shared/src/*"],
      "@rules/*": ["../rules/src/*"]
    }
  },
  "include": ["src", "vite.config.ts"]
}

Frontend vite.config.ts:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  build: {
    outDir: "build",
  },
});

Description:

How can I resolve the SyntaxError in my Node.js + TypeScript backend when importing modules using path aliases defined in tsconfig.json? Is there a way to configure my backend so that it understands the path aliases without adding extra build steps or using tools like ts-node with tsconfig-paths?


Solution

  • I finally resolved it. It is because Node.js doesn't understand the TypeScript path aliases defined in tsconfig.json by default.

    Solution

    1. Create a tsconfig.json in the shared directory and define the aliases there.

      shared/tsconfig.json:

      {
        "compilerOptions": {
          "target": "ES2020",
          "module": "ESNext",
          "lib": ["ES2020"],
          "moduleResolution": "Node",
          "experimentalDecorators": true,
          "declaration": true,
          "baseUrl": ".",
          "paths": {
            "~/*": ["src/*"]
          },
          "noUnusedLocals": true,
          "noUnusedParameters": true,
          "noFallthroughCasesInSwitch": true
        },
        "include": ["src", "**/*.ts"],
        "exclude": ["node_modules"]
      }
      
    2. Make sure each project (frontend, backend, and shared) has "type": "module" in its package.json.

      Example package.json:

      {
        "type": "module",
        ...
      }
      
    3. Verify tsconfig paths in the backend/tsconfig.json: Make sure the backend project’s tsconfig.json includes the correct paths to the shared modules, like so:

      {
        "compilerOptions": {
          "baseUrl": ".",
          "paths": {
            "~/*": ["src/*"],
            "@shared/*": ["../shared/src/*"]
          }
        },
        "include": ["src"]
      }