Skip to content

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 jimple@2.0.0-rc.2
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@2.0.0-rc.2/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("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 and with full support for TypeScript.

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

 
 
 

Contributors