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.
- Features
- Why Dependency Injection?
- Quick Start
- Installation
- Core Concepts
- Advanced Features
- ES6 Proxy Mode
- TypeScript Support
- Modular Configuration with Providers
- API Reference
- Real-World Example
- More Examples
- Extending Jimple
- Performance Tips
- Migration from Other DI Containers
- Documentation
β
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
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
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");
npm install jimple
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/Jimple.umd.js"></script>
ES6 Modules
import Jimple from "jimple";
CommonJS
const Jimple = require("jimple");
AMD
define(["jimple"], function (Jimple) {
// Your code here
});
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 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");
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
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
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;
});
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)
Handle optional services with fallbacks:
container.set("cache", (c) => {
if (c.has("redisConfig")) {
return new RedisCache(c.get("redisConfig"));
}
return new MemoryCache(); // Fallback
});
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
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)
Jimple provides full TypeScript support with interface definitions:
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
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
Organize your container configuration into reusable modules:
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);
// providers/database.js
module.exports.register = function (container) {
container.set("database", (c) => new Database(c.get("dbConfig")));
};
// main.js
container.register(require("./providers/database"));
const { provider } = require("jimple");
module.exports = provider((container) => {
container.set("apiService", (c) => new ApiService(c.get("apiConfig")));
});
module.exports = {
database: provider((c) => {
c.set("database", () => new Database());
}),
cache: provider((c) => {
c.set("cache", () => new Cache());
}),
};
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 |
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
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);
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")}`);
});
// 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]");
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);
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();
});
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();
- 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
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")),
);
- π Interactive Guide - Learn with live examples and tutorials
- π Complete API Reference - Detailed JSDoc documentation with full method signatures and examples
MIT License - see LICENSE file for details.
Happy coding! π