Day#1: Building Clean, Breaking Things: The Unexpected Cost of a Global Exception Filter
The goal seemed trivial: create a global ExceptionFilter
to unify error responses across the backend. A quick win. A small boost in DX. What followed instead was a full day of debugging TypeScript build issues, resolving unexpected behavior in ESM mode, and untangling the consequences of mixing modern monorepo tooling with legacy compiler features.
This post is a breakdown of what went wrong β and how it was ultimately fixed.
The setup
The project stack:
- NestJS with TypeScript in ESM mode
- Turborepo for managing multiple applications and shared libraries
- Path aliases declared in
tsconfig.base.json
- Separate shared library for infrastructure concerns (
libs/filters
) - Testing with Jest, type checking with tsc, running via ts-node
The intention was simple: place the reusable filter in libs/filters
, export it via @tin-filters/all-exceptions.filter
, and import it globally in the app. Nothing revolutionary.
Until everything broke.
What went wrong
The moment the filter was imported using a path alias (@tin-filters/...
), everything started to fail in different places depending on the tool used.
tsc --build
threwTS6059
andTS6307
β complaints about files being outsiderootDir
and not part of the build.ts-node
in ESM mode crashed withERR_MODULE_NOT_FOUND
β ignoringtsconfig.paths
entirely.- Jest threw
ts-jest
warnings and runtime errors β trying to transform.js
files withoutallowJs
.
All because of one decision: combining TypeScript path aliases with project references and ESM.
What was fixed
Over the course of a few iterations, the following simplifications were made:
- π Removed
composite: true
and project references β not needed unless usingtsc --build
- β
Kept global aliases in
tsconfig.base.json
(e.g.,@tin-filters/*
) - π¦ Explicitly included shared libraries in each
tsconfig.json
that needs them (include: ["src", "../../../libs/filters"]
) - π Avoided using aliases at runtime in
ts-node
β replaced with relative imports (../../libs/filters/...
) to avoid ESM issues - π§Ό Cleaned
.js
artifacts fromsrc/
to preventts-jest
warnings - π§ Restored
commitlint
hook usinghusky install
and verified permissions
Key takeaway
If you're not using
tsc --build
, don't usecomposite: true
.
If you want path aliases to βjust workβ, keep them purely TypeScript-side β not at runtime.
Simpler is safer.
Whatβs next
The filter works. The monorepo builds. The aliases resolve.
Next step? Back to building the actual system β now with one fewer architectural landmine in place.