From TypeScript Errors to Architectural Insights: A Code Quality Journey

typescriptarchitecturecode-quality

Part 1 of 2 in the "Code Quality and Analytical Humility" series


Series Overview

This two-part series documents a code quality session that evolved from routine TypeScript fixes into deeper lessons about architecture and analysis.

  • Part 1 (you are here): The technical journey—fixing errors, creating abstractions, discovering boundary layers
  • Part 2: Analysis Mistakes to Architectural Wisdom—the analytical missteps that revealed the architecture's true sophistication

These essays are companions. Part 1 celebrates what went right technically; Part 2 examines what went wrong analytically—and why both matter.


The Context

This story unfolds in a polyglot monorepo using Moon orchestration, with a Hono API (Deno), Next.js frontend (Node), and shared validation package—each with different runtime constraints and Zod version dependencies. The trigger was a CI pipeline failure during a routine moon run api:typecheck execution.

The Problem Emerges

What began as a technical fix evolved into a deep exploration of software architecture, code quality, and sustainable development practices when 10 blocking TypeScript errors surfaced across the API codebase.

The Initial Errors

The typecheck errors spanned three critical areas:

Zod Version Incompatibility

The most complex issue involved @hono/zod-validator incompatibility with Zod 4. The project had upgraded to zod: ^4.1.12, but the Hono middleware still expected Zod 3 patterns:

  • _zod.version.minor type mismatches
  • Deprecated .flatten() method calls
  • Incompatible error handling APIs

Missing Imports and Error Handling

Three locations in apps/api/src/routes/projects.ts lacked proper Zod imports and used outdated error flattening methods.

Schema and Logger Mismatches

Email mapping required messageId fields, and logger instantiation failed interface compliance checks.

The Quick Fix vs. Sustainable Solution Dilemma

The initial approach was straightforward: patch the immediate errors. But deeper analysis revealed patterns worth addressing:

  1. Duplicated Error Handling: Three nearly identical Zod error response blocks
  2. Version Dependency Coupling: Zod 4 compatibility issues affecting middleware choices
  3. Technical Debt Documentation: No clear path for future middleware migration

Code Review as Catalyst

The code review process transformed reactive fixes into proactive architecture:

Error Handling Consolidation

Instead of accepting duplicated error responses, we created a centralized utility:

// packages/validation/src/errors.ts
export function zodErrorResponse(c: Context, error: z.ZodError) {
    return c.json({
        error: "validation_error",
        message: "Invalid request body", 
        details: z.flattenError(error),
    }, 400);
}

This single abstraction reduced 24 lines of duplicated code to 9 lines across three locations—a 62% reduction that scales as the API grows.

Technical Debt Documentation

Rather than hiding the custom Zod validator, we documented it comprehensively:

/**
 * Custom Zod validator for Hono - TEMPORARY WORKAROUND
 *
 * @hono/zod-validator doesn't support Zod 4 yet
 * TODO: Migrate back to @hono/zod-validator when they release Zod 4 support
 * See: https://github.com/honojs/middleware/issues/1148
 *
 * This validator handles all validation locations (json, query, form, param)
 * using Zod 4 compatible patterns.
 */

The Boundary Layer Revelation

The refactoring revealed @repo/validation as a critical architectural boundary:

┌─────────────────────────────────────┐
│ Route Handlers (business logic)     │
├─────────────────────────────────────┤
│ Validation Layer (@repo/validation) │ ← Boundary layer
│ - Zod schemas & error formatting    │
│ - Custom validators                 │
│ - Middleware helpers                │
├─────────────────────────────────────┤
│ Core Types (packages/core)          │
│ - Drizzle schemas                   │
│ - Base Zod schemas                  │
└─────────────────────────────────────┘

This positioning provides three key benefits:

Change Absorption

External dependency changes (Zod versions, middleware updates) get contained within the validation layer, preventing ripple effects through business logic.

Testing Boundaries

Clear separation enables focused testing:

  • Validation layer tests: Input sanitization and error formatting
  • Business logic tests: Domain logic with mocked validation
  • Integration tests: End-to-end request flows

Evolution Flexibility

The Zod 4 migration demonstrated contained, incremental evolution. When @hono/zod-validator adds Zod 4 support, migration involves only the validation package—route handlers remain untouched.

Principles Learned

Three key principles emerged from this exercise:

1. Error Handling Utilities Pay Dividends

Centralized validation responses prevent API contract drift. As the API grows, consistent error handling becomes increasingly valuable for client reliability.

2. Technical Debt Documentation Matters

TODO comments with GitHub issue links transform debt into trackable work items. This creates accountability and enables prioritization alongside feature development.

3. Small Abstractions Compound

The 62% line reduction across three call sites demonstrates compounding returns. What seems minor initially scales significantly as the codebase grows.

4. Consolidate Duplicates at the Right Layer

A concrete example: validateDocumentStatus existed in three locations:

// Location 1: packages/orpc-server/src/lib/utils.ts (good - used Zod enum)
// Location 2: packages/use-cases/src/projects/getProject.ts (duplicate - hardcoded array)
// Location 3: apps/api/src/routes/documents.ts (duplicate - local Set)

The fix wasn't complex—move the canonical version to @indicia/core where the DOCUMENT_STATUS constant lives:

// packages/core/src/domain/constants.ts
export const DOCUMENT_STATUS = {
  UPLOADING: 'uploading',
  PROCESSING: 'processing',
  COMPLETE: 'complete',
  ERROR: 'error',
  DELETED: 'deleted',
} as const

const VALID_DOCUMENT_STATUSES = Object.values(DOCUMENT_STATUS) as readonly string[]

export function validateDocumentStatus(status: string): DocumentStatus {
  return VALID_DOCUMENT_STATUSES.includes(status)
    ? (status as DocumentStatus)
    : DOCUMENT_STATUS.ERROR
}

Then update all three locations to import from core. ~25 lines of duplicate code eliminated, single source of truth established.

Part 2 Preview: These principles emerged cleanly. But the session also produced suggestions that were rejected—improvements that seemed reasonable but missed the mark entirely. Part 2 examines those analytical failures and what they reveal about understanding unfamiliar codebases.

Architectural Patterns Recognized

This boundary layer approach mirrors proven architectural patterns:

  • Hexagonal Architecture: Validation as an adapter isolating core business logic
  • Clean Architecture: Validation as infrastructure, not domain logic
  • Ports & Adapters: Validation layer as a port for external input adaptation

Long-term Impact

The @repo/validation package now serves as a proper validation utilities layer, ready for:

  • Rate limiting error responses
  • Authentication error handling
  • Business logic validation patterns
  • Input sanitization utilities

Tradeoffs

Every architectural decision has costs. The validation layer improvements introduced several tradeoffs:

  • Custom validator maintenance: We now own this code until upstream catches up
  • Abstraction overhead: New developers must learn the validation layer API
  • Migration risk: When @hono/zod-validator supports Zod 4, we need to verify parity

These costs are outweighed by the benefits of clean separation and future flexibility, but they represent real engineering debt that must be tracked and eventually resolved.

Patterns to Watch For

When fixing TypeScript errors, look for opportunities to:

  • Extract repeated patterns into utilities
  • Document workarounds with migration paths
  • Identify boundary layers absorbing external changes

Pre-Refactoring Checklist

Before diving into fixes, consider:

  • [ ] Identify root cause vs. symptoms
  • [ ] Count duplicated patterns
  • [ ] Check for upstream issue tracking
  • [ ] Map affected architectural layers

Conclusion

What began as fixing 10 TypeScript errors evolved into architectural improvement. The journey demonstrated how code review, when approached thoughtfully, can transform routine maintenance into strategic enhancement.

The validation boundary layer created during this process will serve the codebase for years, absorbing external changes while maintaining internal stability. This is the essence of sustainable software development—building systems that evolve gracefully rather than accumulating technical debt.

The experience reinforced that excellent code quality emerges not from following rules, but from recognizing patterns, understanding architectural boundaries, and making thoughtful investments in abstractions that compound over time.


Continue to Part 2

This essay told the success story—the technical wins, the clean abstractions, the architectural insights. But it's only half the picture.

The same session that produced these improvements also produced confident suggestions that were completely wrong. Suggestions based on assumptions about Hono RPC that missed the actual oRPC architecture. Suggestions for features the codebase didn't need.

Part 2: Analysis Mistakes to Architectural Wisdom tells that story—and argues that the failures were ultimately more instructive than the successes.

"Your suggestions were solutions looking for problems. Our architecture doesn't have those problems."

That single piece of feedback transformed the entire session. Part 2 explores why.