Skip to content

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

License

Notifications You must be signed in to change notification settings

fjorgemota/jimple

Repository files navigation

Jimple

GitHub Actions Workflow Status npm version npm downloads node version JSDelivr

Jimple is a lightweight and powerful dependency injection container for JavaScript and TypeScript, built for Node.js and the browser. Inspired by Pimple, it brings clean, flexible dependency management to modern JavaScript projects β€” with zero dependencies and a minimal API.

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 [email protected]
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/[email protected]/dist/Jimple.umd.js"></script>

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;
});

Removing Services or Parameters

Remove services or parameters from the container with unset():

container.set("logger", (c) => new Logger());
container.set("apiUrl", "https://api.example.com");

// Remove a service
container.unset("logger");
console.log(container.has("logger")); // false

// Remove a parameter
container.unset("apiUrl");
console.log(container.has("apiUrl")); // false

// Safe to unset non-existent services
container.unset("nonExistent"); // No error thrown

Important Notes:

  • Removes the service/parameter completely from the container
  • Clears any cached instances and metadata for services
  • Cannot be undone - you'll need to re-register the service
  • Safe to call on non-existent services (no error thrown)

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
unset(id, value) Remove 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("[email protected]");

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 and with full support for TypeScript.

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Contributors 6