DEV Community

Cover image for Architectural Mastery in Node.js: Designing a Robust, Multi-Platform NPM Package
Gaurav Kumar Singh
Gaurav Kumar Singh

Posted on

Architectural Mastery in Node.js: Designing a Robust, Multi-Platform NPM Package

When building cross-platform Node.js applications, developer tools, or CLI utilities, managing environmental variance is a common challenge. Operating systems rely on different binaries, network utilities vary, and error-handling mechanisms change depending on the runtime context.

Handling these variations with deeply nested conditional logic (if/else or switch statements) makes your application state brittle, difficult to test, and painful to scale.

This engineering guide demonstrates how to architect a clean, testable, and robust cross-platform Node.js utility using the Strategy Pattern and the Factory Pattern.

Production Resources & Quick Start

The finished project discussed in this guide is fully implemented, open-source, and available for use in your production pipelines:

npm i @gks101/port-kill

Enter fullscreen mode Exit fullscreen mode

1. The Real-World Engineering Problem: EADDRINUSE

Every developer has encountered the notorious local server collision:

Error: listen EADDRINUSE: address already in use :::3000

Enter fullscreen mode Exit fullscreen mode

To resolve this programmatically across platforms, an application must execute two distinct steps:

  1. Discovery: Identify the Process IDs (PIDs) listening on a target port.
  2. Termination: Safely and effectively kill those processes.

However, the native tools available to execute these actions vary radically across operating systems:

Operating System Process Discovery Mechanism Process Termination Mechanism
macOS / Linux lsof -t -i :[port] or fuser kill -[SIGNAL] [pids]
Windows netstat -ano taskkill /F /T /PID [pid]

The Naive Anti-Pattern

Placing all of this conditional logic inside a single orchestrator function creates a fragile codebase:

// ❌ ANTI-PATTERN: Brittle cross-platform orchestration
export function killPortNaive(port: number) {
  if (process.platform === 'win32') {
    // Windows netstat execution
    // Complex regex string parsing for netstat table rows
    // Individual taskkill subprocess spawning
  } else {
    // Unix lsof execution with fallback to fuser
    // Parsing space-separated or newline-separated PID strings
    // POSIX signal mapping and kill execution
  }
}

Enter fullscreen mode Exit fullscreen mode

This structural anti-pattern couples your core business logic to low-level OS command strings, breaks the Single Responsibility Principle, makes unit testing impossible without complex environment mocking, and introduces significant maintenance risks.


2. Structural Blueprints: Strategy & Factory

To decouple orchestration from platform mechanics, we introduce a clear division of labor using two complementary design patterns:

The Strategy Pattern (Behavioral)

  • Core Philosophy: Defines a family of interchangeable algorithms, encapsulating each one inside a dedicated class that implements a uniform contract.
  • Application Goal: Standardize how the application communicates with the OS layer, regardless of whether it targets a Windows kernel or a POSIX terminal.

The Factory Pattern (Creational)

  • Core Philosophy: Centralizes instantiation logic, abstracting away the concrete classes and configurations required to construct an operational object.
  • Application Goal: Evaluate runtime constraints (such as process.platform) and supply the correct strategy configuration seamlessly to the application layer.

System Architecture Topology

The following diagram illustrates how these patterns isolate responsibilities across the application lifecycle:

   [ Client Layer: CLI / Programmatic API ]
                      │
                      ▼
       [ Core Service Orchestrator ]
                      │
                      ├─► [ PlatformStrategyFactory.create() ]
                      │                │
                      │                ├─► returns WindowsPortStrategy
                      │                └─► returns UnixPortStrategy
                      │
                      ▼
        [ Execution via Abstract Contract ]
          (findPids / terminatePids)
                      │
                      ▼
         [ Platform Command Factory ]
                      │
                      ├─► UnixCommandFactory   ──► [{ binary: 'lsof', args: [...] }]
                      └─► WindowsCommandFactory ──► [{ binary: 'netstat', args: [...] }]
                      │
                      ▼
         [ Decoupled Command Runner ] (spawnSync)

Enter fullscreen mode Exit fullscreen mode

3. Designing Core Domain Contracts and Typings

Before writing structural code, establish clear execution boundaries. Define the input parameters, telemetry outputs, and execution schemas using TypeScript interfaces.

// Domain Types: options and results contracts
export interface PortKillOptions {
  port?: number | number[];
  force?: boolean;
  signal?: 'SIGKILL' | 'SIGTERM' | 'SIGINT' | string;
  verbose?: boolean;
  logger?: (message: string, level?: 'info' | 'warn' | 'error' | 'debug') => void;
  dryRun?: boolean;
}

export interface PortKillResult {
  port: number;
  success: boolean;
  pids: number[];
  message: string;
  error?: string;
  timestamp: string;
}

export interface PlatformCommand {
  binary: string;
  args: string[];
}

export interface CommandRunResult {
  stdout: string;
  success: boolean;
  error?: string;
}

export type PortKillLog = (msg: string, lvl?: 'info' | 'warn' | 'error' | 'debug') => void;

Enter fullscreen mode Exit fullscreen mode

4. Decoupling Execution: The Command Layer

To insulate the system from shell injection vulnerabilities and shell interpolation bugs, avoid raw string executions. Instead, treat shell processes as structured objects executed via a dedicated runner.

Step 1: The Command Factory Interface

This creational boundary ensures that structural variations in CLI syntax remain completely invisible to the operational strategy layer.

export interface PlatformCommandFactory {
  createFindCommands(port: number): PlatformCommand[];
  createKillCommands(pids: number[], signalOrForce: string | boolean): PlatformCommand[];
}

Enter fullscreen mode Exit fullscreen mode

Step 2: Implementing the Unix Command Factory

export class UnixCommandFactory implements PlatformCommandFactory {
  createFindCommands(port: number): PlatformCommand[] {
    return [
      { binary: 'lsof', args: ['-t', '-n', '-i', `:${port}`] },
      { binary: 'fuser', args: [`${port}/tcp`] }
    ];
  }

  createKillCommands(pids: number[], signalOrForce: string | boolean): PlatformCommand[] {
    const signal = typeof signalOrForce === 'string' ? signalOrForce : 'SIGKILL';
    return [
      { binary: 'kill', args: [`-${signal}`, ...pids.map(String)] }
    ];
  }
}

Enter fullscreen mode Exit fullscreen mode

Step 3: Implementing the Windows Command Factory

export class WindowsCommandFactory implements PlatformCommandFactory {
  createFindCommands(port: number): PlatformCommand[] {
    // netstat provides a full connection matrix table
    return [{ binary: 'netstat', args: ['-ano'] }];
  }

  createKillCommands(pids: number[], signalOrForce: string | boolean): PlatformCommand[] {
    const useForce = !!signalOrForce;
    const baseArgs = useForce ? ['/F', '/T'] : ['/T'];

    // Windows taskkill handles targeted process compilation atomically per PID
    return pids.map(pid => ({
      binary: 'taskkill',
      args: [...baseArgs, '/PID', String(pid)]
    }));
  }
}

Enter fullscreen mode Exit fullscreen mode

Step 4: The Stateless Command Runner

The runner handles the underlying mechanics of subprocess creation. It accepts a structured PlatformCommand object and returns a standardized result.

import { spawnSync } from 'child_process';

export function runCommand(command: PlatformCommand, log: PortKillLog): CommandRunResult {
  log(`Executing: ${command.binary} ${command.args.join(' ')}`, 'debug');

  const result = spawnSync(command.binary, command.args, {
    shell: false, // Prevents shell command injection vulnerabilities
    encoding: 'utf8',
    stdio: ['pipe', 'pipe', 'pipe']
  });

  if (!result.error && result.status === 0) {
    return { stdout: result.stdout || '', success: true };
  }

  return {
    stdout: result.stdout || '',
    success: false,
    error: result.error?.message || result.stderr || 'Execution failure'
  };
}

Enter fullscreen mode Exit fullscreen mode

5. Implementing Behavioral Architecture: The Strategy Pattern

With commands decoupled, we establish the algorithmic contract for platform strategy execution.

export interface TerminationResult {
  success: boolean;
  error?: string;
}

export interface PlatformStrategy {
  readonly name: 'unix' | 'windows';
  findPids(port: number, log: PortKillLog): number[];
  terminatePids(pids: number[], options: PortKillOptions, log: PortKillLog): TerminationResult;
}

Enter fullscreen mode Exit fullscreen mode

The Unix Strategy Implementation

This strategy manages Unix-specific workflows, including falling back from lsof to fuser and sanitizing protected system processes.

export class UnixPortStrategy implements PlatformStrategy {
  readonly name = 'unix' as const;

  constructor(private readonly commandFactory: PlatformCommandFactory) {}

  findPids(port: number, log: PortKillLog): number[] {
    const [lsofCmd, fuserCmd] = this.commandFactory.createFindCommands(port);

    // Fallback Sequence Implementation
    const lsofResult = runCommand(lsofCmd, log);
    if (lsofResult.success && lsofResult.stdout.trim()) {
      return this.parsePids(lsofResult.stdout);
    }

    log('lsof failed or returned empty; executing fuser fallback optimization...', 'info');
    const fuserResult = runCommand(fuserCmd, log);
    return this.parsePids(fuserResult.stdout);
  }

  terminatePids(pids: number[], options: PortKillOptions, log: PortKillLog): TerminationResult {
    const targetSignal = options.signal || (options.force ? 'SIGKILL' : 'SIGTERM');
    const filteredPids = this.filterProtectedPids(pids, log);

    if (filteredPids.length === 0) return { success: true };

    const [killCmd] = this.commandFactory.createKillCommands(filteredPids, targetSignal);
    const result = runCommand(killCmd, log);

    return { success: result.success, error: result.error };
  }

  private parsePids(stdout: string): number[] {
    return stdout
      .split(/[\s\n]+/)
      .map(token => parseInt(token.trim(), 10))
      .filter(pid => !isNaN(pid));
  }

  private filterProtectedPids(pids: number[], log: PortKillLog): number[] {
    const currentPid = process.pid;
    const parentPid = process.ppid;
    return pids.filter(pid => {
      if (pid === currentPid || pid === parentPid) {
        log(`Process protection triggered for protected PID: ${pid}`, 'warn');
        return false;
      }
      return true;
    });
  }
}

Enter fullscreen mode Exit fullscreen mode

The Windows Strategy Implementation

This strategy manages Windows-specific flows, handling tabular netstat text parsing and executing targeted taskkill loops.

export class WindowsPortStrategy implements PlatformStrategy {
  readonly name = 'windows' as const;

  constructor(private readonly commandFactory: PlatformCommandFactory) {}

  findPids(port: number, log: PortKillLog): number[] {
    const [netstatCmd] = this.commandFactory.createFindCommands(port);
    const result = runCommand(netstatCmd, log);

    if (!result.success || !result.stdout) return [];

    return this.parseNetstat(result.stdout, port);
  }

  terminatePids(pids: number[], options: PortKillOptions, log: PortKillLog): TerminationResult {
    const forceFlag = options.force ?? true;
    const commands = this.commandFactory.createKillCommands(pids, forceFlag);
    const errors: string[] = [];

    for (const cmd of commands) {
      const res = runCommand(cmd, log);
      if (!res.success) errors.push(res.error || `Failed to terminate PID`);
    }

    return {
      success: errors.length === 0,
      error: errors.length > 0 ? errors.join('; ') : undefined
    };
  }

  private parseNetstat(stdout: string, targetPort: number): number[] {
    const pids = new Set<number>();
    const lines = stdout.split(/[\r\n]+/);

    // Look for matching tokens: Local Address matching :[port]
    const portMatchRegex = new RegExp(`[:\\]]${targetPort}$`);

    for (const line of lines) {
      const parts = line.trim().split(/\s+/);
      if (parts.length < 5) continue;

      const localAddress = parts[1]; 
      const pidToken = parts[parts.length - 1];
      const pid = parseInt(pidToken, 10);

      if (portMatchRegex.test(localAddress) && !isNaN(pid) && pid > 0) {
        pids.add(pid);
      }
    }

    return Array.from(pids);
  }
}

Enter fullscreen mode Exit fullscreen mode

6. Centralizing Instantiation: The Strategy Factory

The factory completely abstracts setup details away from the application workflow. By exposing an optional parameter with a sensible default, we keep production use clean while allowing effortless platform mocking during testing.

export class PlatformStrategyFactory {
  /**
   * Instantiates the correct platform strategy configuration matrix.
   * @param platform Target node environment runtime key override.
   */
  static create(platform: NodeJS.Platform = process.platform): PlatformStrategy {
    if (platform === 'win32') {
      return new WindowsPortStrategy(new WindowsCommandFactory());
    }

    // Default runtime routing maps seamlessly to POSIX environments (Linux/macOS)
    return new UnixPortStrategy(new UnixCommandFactory());
  }
}

Enter fullscreen mode Exit fullscreen mode

7. Orchestration Layer Implementation

The orchestrator manages the high-level business workflow. It coordinates logging, queries the factory for the proper strategy, handles dryRun evaluation, and builds the final telemetry output.

export function killSinglePort(port: number, options: PortKillOptions = {}): PortKillResult {
  const timestamp = new Date().toISOString();
  const log: PortKillLog = options.logger || ((msg, lvl = 'info') => {
    if (options.verbose) console.log(`[${lvl.toUpperCase()}] ${msg}`);
  });

  // The Orchestrator interacts solely with abstract operational models
  const strategy = PlatformStrategyFactory.create();
  log(`Detected platform strategy context: ${strategy.name}`, 'info');

  const pids = strategy.findPids(port, log);

  if (pids.length === 0) {
    return { port, success: true, pids: [], message: `Port ${port} is clear.`, timestamp };
  }

  if (options.dryRun) {
    return { 
      port, 
      success: true, 
      pids, 
      message: `Dry-run execution mode. Identified target processes: ${pids.join(', ')}`, 
      timestamp 
    };
  }

  const termination = strategy.terminatePids(pids, options, log);

  return {
    port,
    success: termination.success,
    pids,
    message: termination.success 
      ? `Successfully cleared port ${port} by terminating PIDs: ${pids.join(', ')}`
      : `Partial or total failure clearing port ${port}. Error encountered: ${termination.error}`,
    error: termination.error,
    timestamp
  };
}

Enter fullscreen mode Exit fullscreen mode

8. Exposing Clean Programmatic and CLI Interfaces

Because the factory and strategy mechanics absorb all cross-platform complexities internally, your external interfaces remain clean and user-friendly.

The Programmatic Module API

Downstream developers can consume your module cleanly using modern async/await patterns:

export function portKillSync(ports: number | number[], options: PortKillOptions = {}): PortKillResult[] {
  const targets = Array.isArray(ports) ? ports : [ports];
  return targets.map(port => {
    try {
      return killSinglePort(port, options);
    } catch (err: any) {
      return {
        port,
        success: false,
        pids: [],
        message: `Unhandled runtime error targeting port ${port}`,
        error: err?.message || String(err),
        timestamp: new Date().toISOString()
      };
    }
  });
}

export async function portKill(ports: number | number[], options: PortKillOptions = {}): Promise<PortKillResult[]> {
  return new Promise((resolve) => {
    process.nextTick(() => resolve(portKillSync(ports, options)));
  });
}

Enter fullscreen mode Exit fullscreen mode

The Thin CLI Wrapper

#!/usr/bin/env node
import { portKillSync } from './index';

export function parseAndRunCli(args: string[]): void {
  const rawPorts = args.filter(arg => !arg.startsWith('--'));
  const flags = args.filter(arg => arg.startsWith('--'));

  if (rawPorts.length === 0 || flags.includes('--help')) {
    console.log(`
      Usage: port-kill <port1> <port2> [options]
      Options:
        --force    Force process termination
        --verbose  Enable extended telemetry logs
        --dry-run  Discover target processes without terminating them
    `);
    process.exit(0);
  }

  const targetPorts = rawPorts.map(Number);
  const options = {
    force: flags.includes('--force'),
    verbose: flags.includes('--verbose'),
    dryRun: flags.includes('--dry-run'),
  };

  const operationalReports = portKillSync(targetPorts, options);
  const exitCode = operationalReports.every(r => r.success) ? 0 : 1;

  console.table(operationalReports);
  process.exit(exitCode);
}

parseAndRunCli(process.argv.slice(2));

Enter fullscreen mode Exit fullscreen mode

9. Deterministic Unit Testing Strategy

By isolating implementation details behind clean interfaces, you can thoroughly unit test each strategy and factory without needing multiple physical environments.

import { PlatformStrategyFactory } from './platform/factory';
import { WindowsPortStrategy } from './platform/strategies';
import { UnixPortStrategy } from './platform/strategies';

describe('Design Pattern Strategy & Factory Verification Suite', () => {

  it('should instantiate the correct platform strategy based on environment signatures', () => {
    const winStrategy = PlatformStrategyFactory.create('win32');
    expect(winStrategy).toBeInstanceOf(WindowsPortStrategy);
    expect(winStrategy.name).toBe('windows');

    const darwinStrategy = PlatformStrategyFactory.create('darwin');
    expect(darwinStrategy).toBeInstanceOf(UnixPortStrategy);
    expect(darwinStrategy.name).toBe('unix');
  });

  it('should execute isolated tests on Unix PID parsing logic without environmental side effects', () => {
    const mockStrategy = PlatformStrategyFactory.create('linux') as UnixPortStrategy;
    // You can now inject custom mock runners or sample standard output structures 
    // to verify parsing behavior deterministically without affecting the host runtime.
  });
});

Enter fullscreen mode Exit fullscreen mode

Summary Mental Model

To effectively apply these structural patterns to your future Node.js projects, keep this clear separation of concerns in mind:

  • The Factory decides: It assesses runtime context to construct the proper operational dependencies.
  • The Strategy behaves: It encapsulates environment-specific rules, syntax, and data parsing logic behind a unified contract.
  • The Service coordinates: It manages core business logic, handles option states (like dryRun), and acts as the workflow engine.
  • The Runner executes: It isolates low-level subprocess spawning safely and uniformly across the codebase.

Explore the Live Implementation

To review the complete production codebase, run validation suites, or check integration examples, explore the following resources:

Top comments (0)