Skip to content

fjorgemota/jimple

Repository files navigation

Jimple

GitHub Actions Workflow Status npm version npm downloads node version JSDelivr

A lightweight, powerful dependency injection container for Node.js and browsers. Jimple is a JavaScript port of the popular Pimple DI container from PHP, bringing clean dependency management to your JavaScript projects.

Table of Contents

Features

βœ… Lightweight - ~1KB minified and gzipped
βœ… Zero dependencies - No external dependencies in Node.js
βœ… Universal - Works in Node.js and browsers
βœ… TypeScript - Fully typed with excellent IDE support
βœ… ES6 Proxy support - Modern syntax with property access
βœ… Extensible - Easy to extend and customize
βœ… Well tested - 100% code coverage
βœ… Stable API - Mature, stable API you can depend on

Why Dependency Injection?

Dependency injection helps you write more maintainable, testable code by:

  • Decoupling components - Services don't need to know how their dependencies are created
  • Improving testability - Easy to swap dependencies with mocks during testing
  • Managing complexity - Centralized configuration of how objects are wired together
  • Lazy loading - Services are only created when needed
  • Singleton by default: Same instance returned on subsequent calls
  • Dependency management: Services can depend on other services

Quick Start

npm install jimple
import Jimple from 'jimple';

// Create container
const container = new Jimple();

// Define a simple service
container.set('logger', (c) => {
  return {
    log: (msg) => console.log(`[${new Date().toISOString()}] ${msg}`)
  };
});

// Define a service that depends on another
container.set('userService', (c) => {
  const logger = c.get('logger');
  return {
    createUser: (name) => {
      logger.log(`Creating user: ${name}`);
      return { id: Math.random(), name };
    }
  };
});

// Use your services
const userService = container.get('userService');
const user = userService.createUser('Alice');

Installation

npm

npm install jimple

CDN (Browser)

<script src="https://cdn.jsdelivr.net/npm/jimple@latest/src/Jimple.js"></script>

⚠️ Production Warning: Replace latest with a specific version for production use.

Import Methods

ES6 Modules

import Jimple from "jimple";

CommonJS

const Jimple = require("jimple");

AMD

define(["jimple"], function(Jimple) {
  // Your code here
});

Core Concepts

Services

Services are objects that perform tasks in your application. They're defined as functions that return the service instance:

// Database connection service
container.set('database', (c) => {
  const config = c.get('dbConfig');
  return new Database(config.host, config.port);
});

// Email service that depends on database
container.set('emailService', (c) => {
  const db = c.get('database');
  return new EmailService(db);
});

Parameters

Parameters store configuration values, strings, numbers, or any non-function data:

// Configuration parameters
container.set('dbConfig', {
  host: 'localhost',
  port: 5432,
  database: 'myapp'
});

container.set('apiKey', 'abc123');
container.set('isProduction', process.env.NODE_ENV === 'production');

Factory Services

When you need a new instance every time instead of a singleton:

container.set('httpRequest', container.factory((c) => {
  const config = c.get('httpConfig');
  return new HttpRequest(config);
}));

// Each call returns a new instance
const req1 = container.get('httpRequest');
const req2 = container.get('httpRequest'); // Different instance

Advanced Features

Protecting Functions

To store an actual function (not a service factory) as a parameter:

container.set('utility', container.protect(() => {
  return Math.random() * 100;
}));

const utilityFn = container.get('utility'); // Returns the function itself
const result = utilityFn(); // Call the function

Extending Services

Add behavior to existing services:

container.set('logger', (c) => new Logger());

// Extend the logger to add file output
container.extend('logger', (logger, c) => {
  logger.addFileHandler('/var/log/app.log');
  return logger;
});

Optional Dependencies & Defaults

Handle optional services with fallbacks:

container.set('cache', (c) => {
  if (c.has('redisConfig')) {
    return new RedisCache(c.get('redisConfig'));
  }
  return new MemoryCache(); // Fallback
});

Raw Service Access

Get the service definition function instead of the service itself:

container.set('database', (c) => new Database());

const dbFactory = container.raw('database');
const db1 = dbFactory(container);
const db2 = dbFactory(container); // Create another instance manually

ES6 Proxy Mode

Use modern JavaScript syntax for a more natural API:

const container = new Jimple();

// Set services using property syntax
container['logger'] = (c) => new Logger();
container['userService'] = (c) => new UserService(c['logger']);

// Access services as properties
const userService = container.userService;

Limitations:

  • Can't overwrite built-in methods (set, get, etc.)
  • Accessing non-existent properties throws an error
  • TypeScript requires special handling (see below)

TypeScript Support

Jimple provides full TypeScript support with interface definitions:

Basic TypeScript Usage

interface Services {
  logger: Logger;
  database: Database;
  userService: UserService;
  apiKey: string;
}

const container = new Jimple<Services>();

container.set('apiKey', 'secret-key');
container.set('logger', (c) => new Logger());
container.set('database', (c) => new Database());
container.set('userService', (c) =>
        new UserService(c.get('logger'), c.get('database'))
);

// Type-safe access
const userService: UserService = container.get('userService'); // βœ…
const wrong: Database = container.get('userService'); // ❌ Compile error

TypeScript with Proxy Mode

interface Services {
  logger: Logger;
  userService: UserService;
}

const container = Jimple.create<Services>({
  logger: (c) => new Logger(),
  userService: (c) => new UserService(c.logger)
});

const userService: UserService = container.userService; // βœ… Type-safe

Note: Due to TypeScript limitations with proxies, you can't set properties directly. Use the set method instead:

container.set('newService', (c) => new Service()); // βœ… Works
container.newService = (c) => new Service();       // ❌ TypeScript error

Modular Configuration with Providers

Organize your container configuration into reusable modules:

Basic Provider

const databaseProvider = {
  register(container) {
    container.set('dbConfig', {
      host: process.env.DB_HOST ?? 'localhost',
      port: process.env.DB_PORT ?? 5432
    });

    container.set('database', (c) => {
      const config = c.get('dbConfig');
      return new Database(config);
    });
  }
};

container.register(databaseProvider);

File-based Providers (Node.js)

// providers/database.js
module.exports.register = function(container) {
  container.set('database', (c) => new Database(c.get('dbConfig')));
};

// main.js
container.register(require('./providers/database'));

Provider Helper

const { provider } = require("jimple");

module.exports = provider((container) => {
  container.set('apiService', (c) => new ApiService(c.get('apiConfig')));
});

Multiple Named Providers

module.exports = {
  database: provider((c) => {
    c.set('database', () => new Database());
  }),
  cache: provider((c) => {
    c.set('cache', () => new Cache());
  })
};

API Reference

Container Methods

Method Description Returns
set(id, value) Define a service or parameter void
get(id) Retrieve a service or parameter any
has(id) Check if service/parameter exists boolean
factory(fn) Create a factory service fn
protect(fn) Protect a function from being treated as service fn
extend(id, fn) Extend an existing service void
raw(id) Get the raw service definition Function
register(provider) Register a service provider void

Provider Interface

const provider = {
  register(container) {
    // Define services and parameters
  }
};

πŸ“š For complete API documentation with detailed examples, see the full API reference

πŸ“š For complete API documentation with detailed examples, see the full API reference

Real-World Example

Here's a more comprehensive example showing how to structure a web application:

import Jimple from 'jimple';

const container = new Jimple();

// Configuration
container.set('config', {
  database: {
    host: process.env.DB_HOST ?? 'localhost',
    port: process.env.DB_PORT ?? 5432
  },
  server: {
    port: process.env.PORT ?? 3000
  }
});

// Infrastructure services
container.set('database', (c) => {
  const config = c.get('config').database;
  return new Database(config.host, config.port);
});

container.set('logger', (c) => {
  return new Logger(c.get('config').logLevel);
});

// Business services
container.set('userRepository', (c) => {
  return new UserRepository(c.get('database'));
});

container.set('userService', (c) => {
  return new UserService(
          c.get('userRepository'),
          c.get('logger')
  );
});

// HTTP services
container.set('userController', (c) => {
  return new UserController(c.get('userService'));
});

container.set('server', (c) => {
  const config = c.get('config').server;
  const app = new ExpressApp();

  app.use('/users', c.get('userController').routes());

  return app;
});

// Start the application
const server = container.get('server');
server.listen(container.get('config').server.port);

More Examples

Express.js Web Server

import express from 'express';
import Jimple from 'jimple';

const container = new Jimple();

// Configuration
container.set('port', process.env.PORT ?? 3000);
container.set('corsOrigins', process.env.CORS_ORIGINS?.split(',') ?? ['http://localhost:3000']);

// Services
container.set('app', (c) => {
  const app = express();
  app.use(express.json());
  return app;
});

container.set('cors', (c) => {
  return (req, res, next) => {
    const origin = req.headers.origin;
    if (c.get('corsOrigins').includes(origin)) {
      res.header('Access-Control-Allow-Origin', origin);
    }
    next();
  };
});

container.set('userController', (c) => {
  return {
    getUsers: (req, res) => res.json([{ id: 1, name: 'Alice' }]),
    createUser: (req, res) => res.json({ id: 2, ...req.body })
  };
});

// Setup routes
container.set('server', (c) => {
  const app = c.get('app');
  const cors = c.get('cors');
  const userController = c.get('userController');

  app.use(cors);
  app.get('/users', userController.getUsers);
  app.post('/users', userController.createUser);

  return app;
});

// Start server
const server = container.get('server');
server.listen(container.get('port'), () => {
  console.log(`Server running on port ${container.get('port')}`);
});

Testing with Mocks

// Production container
const container = new Jimple();
container.set('emailService', (c) => new RealEmailService(c.get('apiKey')));
container.set('userService', (c) => new UserService(c.get('emailService')));

// Test container with mocks
const testContainer = new Jimple();
testContainer.set('emailService', () => ({
  send: jest.fn().mockResolvedValue({ success: true })
}));
testContainer.set('userService', (c) => new UserService(c.get('emailService')));

// Your tests use the mock
const userService = testContainer.get('userService');
await userService.registerUser('test@example.com');

Plugin Architecture

const container = new Jimple();

// Core services
container.set('eventBus', () => new EventEmitter());
container.set('pluginManager', (c) => new PluginManager(c.get('eventBus')));

// Plugin provider
const analyticsPlugin = {
  register(container) {
    container.set('analytics', (c) => {
      const analytics = new Analytics();
      const eventBus = c.get('eventBus');

      eventBus.on('user.created', (user) => analytics.track('user_signup', user));
      eventBus.on('user.login', (user) => analytics.track('user_login', user));

      return analytics;
    });
  }
};

container.register(analyticsPlugin);

Environment-Specific Configuration

const container = new Jimple();
container.set('env', process.env.NODE_ENV ?? 'development');

// Base configuration
container.set('baseConfig', {
  database: { poolSize: 10 },
  cache: { ttl: 3600 }
});

container.set('database', (c) => {
  if (c.get('env') === 'production') {
    const config = { ...c.get('baseConfig').database, poolSize: 50 };
    return new PostgresDatabase(config);
  }
  return new SQLiteDatabase(':memory:');
});

container.set('cache', (c) => {
  if (c.get('env') === 'production') {
    return new RedisCache(process.env.REDIS_URL);
  }
  return new MemoryCache();
});

Extending Jimple

You can create custom container classes:

class MyContainer extends Jimple {
  constructor() {
    super();
    this.loadDefaultServices();
  }

  loadDefaultServices() {
    this.set('logger', () => new DefaultLogger());
  }

  // Add custom methods
  getLogger() {
    return this.get('logger');
  }
}

const container = new MyContainer();

Performance Tips

  • Use factories sparingly - Only when you truly need new instances
  • Lazy load expensive services - Services are created only when needed
  • Organize with providers - Split configuration into logical modules
  • Avoid circular dependencies - Design services to avoid circular references

Migration from Other DI Containers

From Manual Dependency Management

Before:

const logger = new Logger();
const database = new Database(config);
const userService = new UserService(logger, database);

After:

container.set('logger', () => new Logger());
container.set('database', (c) => new Database(c.get('config')));
container.set('userService', (c) =>
        new UserService(c.get('logger'), c.get('database'))
);

Documentation

License

MIT License - see LICENSE file for details.


Happy coding! πŸŽ‰

About

A lightweight dependency injection container for Node.js and browsers, built with modern ES6 features.

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Contributors 6