A lightweight, modular system for Node.js applications with a hybrid approach using ECMAScript for the core and TypeScript for packages.
The Node.js Modular System is designed around a core-and-modules architecture that promotes separation of concerns, code reusability, and maintainability. The architecture consists of:
-
Core System: Written in ECMAScript (JavaScript), the core provides the fundamental infrastructure:
- Module registration and lifecycle management
- Dependency resolution and initialization
- Framework abstraction layer
- Service discovery mechanism
-
Module Packages: Written in TypeScript, modules encapsulate specific functionality:
- Each module is self-contained with its own configuration
- Modules declare dependencies on other modules
- Modules expose services for other modules to consume
- Modules can define routes for API endpoints
-
Framework Adapter: Provides a consistent interface regardless of the underlying web framework:
- Abstracts framework-specific details
- Allows switching between Express and Fastify
- Maintains consistent API for route handlers
- Handles middleware and error management
-
Application Context: Serves as the runtime environment for modules:
- Provides access to services from other modules
- Manages configuration
- Facilitates inter-module communication
The Node.js Modular System is built on several key principles:
-
Modularity: Each piece of functionality is encapsulated in a self-contained module that can be developed, tested, and deployed independently.
-
Explicit Dependencies: Modules must explicitly declare their dependencies, enabling proper initialization order and preventing hidden coupling.
-
Interface-Based Design: Modules interact through well-defined interfaces (services), not implementation details.
-
Framework Agnosticism: Business logic is separated from the web framework, allowing for framework changes without affecting module functionality.
-
Progressive Disclosure: Simple use cases are straightforward, while advanced scenarios are possible without added complexity for basic users.
-
Type Safety: TypeScript is used for modules to provide type checking, better tooling, and improved developer experience.
-
Standardized Structure: Consistent module structure makes the system easier to learn and navigate.
The modular architecture provides several advantages:
-
Scalable Development: Multiple teams can work on different modules simultaneously without stepping on each other's toes.
-
Maintainable Codebase: Smaller, focused modules are easier to understand, test, and maintain.
-
Flexible Deployment: Modules can be deployed together as a monolith or separately as microservices.
-
Framework Flexibility: Switch between Express and Fastify (or add support for other frameworks) without rewriting business logic.
-
Incremental Adoption: Start with a few modules and add more as needed, without major refactoring.
-
Reusable Components: Modules can be shared across projects or published to npm.
-
Clear Boundaries: Well-defined module interfaces prevent tight coupling and make the system more robust.
This system was developed to address common challenges in Node.js application development:
-
Monolithic Complexity: As Node.js applications grow, they often become unwieldy monoliths. This system provides structure without sacrificing flexibility.
-
Framework Lock-in: Many applications become tightly coupled to their web framework. This system's abstraction layer reduces switching costs.
-
TypeScript Integration: While TypeScript offers many benefits, integrating it into existing JavaScript projects can be challenging. This hybrid approach allows for incremental adoption.
-
Dependency Management: Managing dependencies between different parts of an application is often ad-hoc. This system formalizes dependency declaration and resolution.
-
Service Discovery: Finding and using services provided by different parts of an application typically relies on imports or global objects. This system provides a structured service discovery mechanism.
-
Consistent Patterns: Teams often struggle to maintain consistent patterns across a codebase. This system enforces a standard structure while allowing flexibility within modules.
- Simple Module System: Easy-to-understand module registration and initialization
- Framework Agnostic: Works with Express by default, but can be adapted to other frameworks
- Dependency Management: Modules can declare dependencies on other modules
- Service Discovery: Modules can expose and consume services from other modules
- TypeScript Support: Packages are written in TypeScript for type safety and better developer experience
- Publishable Packages: Each package can be published to npm independently
nodejs-modular-system/
├── core/ # Core system (ECMAScript)
│ └── src/
│ ├── modules.js # Module registration and initialization
│ └── framework.js # Framework adapter
│
├── packages/ # Module packages (TypeScript)
│ ├── logger/ # Logger module
│ │ ├── src/
│ │ │ └── index.ts # Module implementation
│ │ ├── package.json # Package configuration
│ │ └── tsconfig.json # TypeScript configuration
│ │
│ ├── users/ # Users module
│ │ ├── src/
│ │ │ └── index.ts # Module implementation
│ │ ├── package.json # Package configuration
│ │ └── tsconfig.json # TypeScript configuration
│ │
│ └── products/ # Products module
│ ├── src/
│ │ └── index.ts # Module implementation
│ ├── package.json # Package configuration
│ └── tsconfig.json # TypeScript configuration
│
├── app.js # Main application entry point
└── package.json # Project configuration
- Install dependencies:
npm install- Build the TypeScript packages:
npm run build- Start the application:
npm startThe server will start on port 3000 by default.
GET /api/users- Get all usersGET /api/users/:id- Get user by IDPOST /api/users- Create a new userPUT /api/users/:id- Update a userDELETE /api/users/:id- Delete a userGET /api/products- Get products information
You can use the CLI tool to create a new module:
npm run create-module my-moduleOr manually create a new directory in the packages folder:
mkdir -p packages/my-module/src- Create a
package.jsonfile:
{
"name": "@nodejs-modular-system/my-module",
"version": "1.0.0",
"description": "My module for the modular system",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"scripts": {
"build": "tsc",
"test": "jest"
},
"keywords": [
"module"
],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^18.15.11",
"typescript": "^5.0.4"
}
}- Create a
tsconfig.jsonfile:
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"declaration": true,
"outDir": "dist",
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}- Create a module implementation in
src/index.ts:
/**
* My module implementation
*/
// Define the module type
interface ModuleDefinition {
id: string;
name: string;
dependencies?: string[];
initialize?: (context: any) => void | Promise<void>;
routes?: (app: any) => void;
services?: Record<string, any>;
}
// My module definition
const myModule: ModuleDefinition = {
id: 'my-module',
name: 'My Module',
// Dependencies
dependencies: ['logger'],
// Initialize function
initialize(context) {
const logger = context.getService('logger');
logger.info('My module initialized');
},
// Define routes
routes(app) {
app.get('/api/my-module', (req, res) => {
return { message: 'Hello from my module!' };
});
},
// Exposed services
services: {
doSomething(data) {
return `Processed: ${data}`;
}
}
};
export default myModule;- Register your module in
app.js:
import myModule from './packages/my-module/dist/index.js';
registerModule('my-module', myModule);Each package can be published to npm independently:
cd packages/my-module
npm publishThe system uses environment variables for configuration. A sample .env.example file is provided:
-
Copy the example file to create your own configuration:
cp .env.example .env
-
Edit the
.envfile to customize your settings:PORT=3000 FRAMEWORK=express DEBUG=true NODE_ENV=development -
The environment variables are used in both:
- Direct application execution via
npm start - Docker Compose configuration for containerized development
- Direct application execution via
Available environment variables:
PORT: Server port (default: 3000)FRAMEWORK: Web framework to use ('express' or 'fastify', default: 'express')DEBUG: Enable debug logging (any value)NODE_ENV: Environment ('development', 'production', etc.)MONGODB_URI: MongoDB connection string (for modules that use MongoDB)
- E-commerce Application Guide - Step-by-step guide for building an e-commerce application using this modular system with Fastify