I was working on a website analysis tool that processed large datasets through a command-line interface when a client asked for API access. The CLI worked perfectly, it could analyze hundreds of companies, capture screenshots, and run AI analysis, but they needed to integrate it into their web platform.
Rather than rewriting the entire application, I discovered a clean approach that wraps existing CLI logic with a production-ready API layer. This method preserves 100% of your original code while adding enterprise-grade capabilities like background job processing, progress tracking, and file uploads. Here's the exact implementation process I developed.
Understanding the Wrapper Approach
The key insight is treating your CLI as a black box that the API orchestrates, rather than trying to refactor internal logic. Your existing CLI handles the complex business operations, while the API layer manages HTTP requests, file uploads, and job coordination.
This separation means your core functionality remains unchanged and testable, while the API provides the interface your applications need. Let's start with the basic wrapper implementation.
Setting Up the Express Foundation
First, create a dedicated API directory structure that keeps your original source code untouched:
These TypeScript interfaces establish clear contracts between your API and consumers. The AnalyzeRequest handles file uploads, while JobStatusResponse provides real-time progress updates for long-running operations.
Next, implement the basic Express server structure:
This creates a minimal Express foundation with proper error handling and CORS support. The registerRoutes function will connect your API endpoints to the underlying CLI functionality.
Implementing Clean Route Architecture
Rather than cramming everything into a single file, organize your API using the controller pattern:
The route definition stays clean and focused, delegating business logic to controllers. The uploadMiddleware handles file processing, while the controller manages the interaction with your CLI.
Now implement the controller that bridges HTTP requests to your CLI commands:
The controller handles HTTP-specific concerns like request validation and response formatting, while delegating the actual work to services. This keeps your API logic testable and maintains clear separation of responsibilities.
Connecting to Your Existing CLI
The service layer is where the magic happens, this is where you invoke your existing CLI commands without modifying them:
typescript
// File: api/services/analysisService.tsimport { spawn } from'child_process';
import { AnalyzeResponse } from'../types/api';
exportclassAnalysisService {
asyncstartAnalysis(file: Express.Multer.File): Promise<AnalyzeResponse> {
// Generate unique job IDconst jobId = `job_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Start your existing CLI processthis.executeCliCommand(file.path, jobId);
return {
jobId,
status: 'processing',
message: 'Analysis started',
originalFileName: file.originalname
};
}
privateexecuteCliCommand(csvPath: string, jobId: string) {
const childProcess = spawn('npm', ['run', 'start', csvPath], {
stdio: 'inherit',
detached: true
});
childProcess.unref(); // Allow parent to exit independently// Store process reference for status trackingthis.trackProcess(jobId, childProcess);
}
privatetrackProcess(jobId: string, process: any) {
// Implementation depends on your needs// Could store in memory, database, or file system
}
}
This approach launches your existing CLI as a child process, allowing the API to return immediately while your analysis runs in the background. The spawn method gives you full control over the CLI execution without requiring any changes to your original code.
The key benefit is that your CLI continues working exactly as before, all the complex logic for website discovery, screenshot capture, and AI analysis remains untouched. The API simply orchestrates when and how these processes run.
The queue service abstracts Redis complexity while providing reliable job processing. Jobs are persisted, can be retried on failure, and provide real-time progress updates.
Implement a background worker that executes your CLI commands:
The worker runs as a separate process, pulling jobs from Redis and executing your CLI commands. This provides horizontal scalability, you can run multiple workers across different machines to handle increased load.
This Docker setup provides a complete production environment with Redis persistence, automatic restarts, and separate API and worker services. Your CLI logic runs inside containers while maintaining all its original functionality.
The Dockerfile builds both the API server and worker from the same codebase:
dockerfile
# File: Dockerfile
FROM node:22-alpine
WORKDIR /app
RUN npm install -g pnpm
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN mkdir -p /app/runs /app/uploads
EXPOSE 3000
This approach means you deploy once and get both HTTP API access and reliable background processing for your existing CLI functionality.
Making API Calls
With everything set up, your API provides clean endpoints for integration:
The API handles file uploads, manages job queues, and provides progress tracking while your original CLI does all the heavy lifting. Client applications get professional API access without you rewriting proven business logic.
Conclusion
Converting a CLI tool to a production API doesn't require rewriting your core functionality. By implementing a clean wrapper architecture with Express.js routes, controllers, and services, you preserve your existing logic while adding enterprise capabilities.
The optional Redis extension provides job queues, progress tracking, and horizontal scalability for production workloads. Docker packaging ensures consistent deployment across environments.
This approach gave me API access to complex website analysis functionality in days rather than weeks, and the same pattern works for any CLI application. You keep your battle-tested business logic while gaining the flexibility of web service integration.
Let me know in the comments if you have questions about implementing this pattern, and subscribe for more practical development guides.