Docs·4ff474d·Updated Mar 14, 2026·43 ADRs
Back
ADR-028accepted
ADR-028: NPM Workspace Package for Shared Code in Docker
ADR-028: NPM Workspace Package for Shared Code in Docker
Date: 2026-01-30 Status: Accepted Decision Makers: Engineering Team
Context
Services were failing at runtime with "Cannot use import statement outside a module" errors when importing from @karmyq/shared. Investigation revealed multiple issues with how the shared package was being built and deployed in Docker containers.
Problems Encountered
- TypeScript source imported instead of compiled JavaScript: Production containers had both
.tssource files and compiled.jsfiles, causing Node.js to resolve incorrectly - Missing TypeScript type definitions:
@types/express,@types/pg,@types/nodemissing from shared package devDependencies - Incorrect tsconfig rootDir: Services compiling to
dist/src/instead ofdist/ - Module resolution timing: npm install ran before compiled shared package was available
- Platform-specific code in builds:
api/client.tsincluded in server builds despite requiring browser-only dependencies
Decision
Implement @karmyq/shared as a proper npm workspace package with the following requirements:
1. Shared Package Configuration
packages/shared/tsconfig.json:
{
"compilerOptions": {
"rootDir": ".",
"outDir": "./dist",
// ... other options
},
"include": ["**/*"],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"api/client.ts",
"api/mobile-storage.ts",
"api/web-storage.ts"
]
}
packages/shared/package.json:
{
"name": "@karmyq/shared",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./utils/logger": {
"types": "./dist/utils/logger.d.ts",
"default": "./dist/utils/logger.js"
}
// ... more exports
},
"scripts": {
"build": "tsc"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.10.0",
"@types/pg": "^8.10.9"
// ... other types
}
}
2. Service tsconfig Configuration
services/{service}/tsconfig.json:
{
"compilerOptions": {
"rootDir": "./src", // CRITICAL: prevents dist/src/
"outDir": "./dist"
},
"include": ["src/**/*"], // NOT tests/**/*
"exclude": ["node_modules", "dist", "../shared"]
}
3. Dockerfile Build Order (TypeScript Services)
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
# 1. Copy workspace package.json
COPY package.json package-lock.json* ./
# 2. Copy package declarations
COPY packages/shared/package*.json ./packages/shared/
COPY services/{service}/package*.json ./services/{service}/
# 3. Install dependencies
RUN npm install
# 4. Build shared package FIRST
COPY packages/shared/ ./packages/shared/
RUN rm -f ./packages/shared/api/client.ts \
./packages/shared/api/mobile-storage.ts \
./packages/shared/api/web-storage.ts
WORKDIR /app/packages/shared
RUN npm run build
# 5. Build service
COPY services/{service}/ /app/services/{service}/
WORKDIR /app/services/{service}
RUN npm run build
# Production stage
FROM node:18-alpine
WORKDIR /app
# 1. Copy workspace root
COPY package.json package-lock.json* ./
# 2. Copy COMPILED shared package BEFORE npm install
COPY --from=builder /app/packages/shared/package.json ./packages/shared/package.json
COPY --from=builder /app/packages/shared/dist ./packages/shared/dist
# 3. Copy service package.json
COPY services/{service}/package*.json ./services/{service}/
# 4. Install production dependencies (workspace sees compiled dist/)
RUN npm install --omit=dev
# 5. Copy compiled service
COPY --from=builder /app/services/{service}/dist ./services/{service}/dist
WORKDIR /app/services/{service}
CMD ["node", "dist/index.js"]
4. Special Case: JavaScript-Only Services (geocoding)
# Production stage
FROM node:18-alpine
WORKDIR /app
# Copy package.json
COPY services/geocoding-service/package*.json ./
# Install dependencies directly (NOT as workspace)
RUN npm install --omit=dev
# Copy service source
COPY services/geocoding-service/index.js ./index.js
CMD ["node", "./index.js"]
Consequences
Positive
- Proper module resolution: Services correctly import compiled JavaScript
- Type safety during development: TypeScript has all type definitions
- Consistent build output: All services compile to
dist/index.js - Faster Docker builds: Shared package built once, reused by all services
- Clear separation: Platform-specific code excluded from server builds
Negative
- Build complexity: Multi-stage builds with specific ordering required
- Dockerfile duplication: Similar patterns across all service Dockerfiles
- Debugging difficulty: Build failures can be hard to trace through multiple stages
Neutral
- Workspace awareness required: Developers must understand npm workspaces
- Build order dependency: Shared package must build before services
Implementation Checklist
When creating a new TypeScript service:
- Add
"rootDir": "./src"to tsconfig.json - Set
"include": ["src/**/*"](exclude tests) - Copy Dockerfile from existing TypeScript service
- Verify
npm run buildproducesdist/index.js(notdist/src/index.js) - Test Docker build:
docker build -t test-service -f services/{service}/Dockerfile . - Verify runtime: imports resolve to
@karmyq/shared/dist/*.js
When modifying shared package:
- Ensure all type dependencies in devDependencies
- Exclude platform-specific files from tsconfig
- Update package.json exports for new modules
- Run
npm run buildin shared package - Rebuild dependent services to test
Lessons Learned
- Copy order matters: Shared package dist/ must exist before
npm installin production stage - Empty arrays don't override:
volumes: []in docker-compose doesn't clear volumes; usevolumes: ~ - TypeScript rootDir is critical: Without it, directory structure is preserved in output
- Workspace resolution is Node.js, not TypeScript: package.json exports control runtime resolution
Related ADRs
- ADR-004: Microservices + Event-Driven Architecture
- ADR-023: Infrastructure Standardization