@silay/core
v1.0.3
Published
Silay is a **mini-framework for Node.js**, built on top of Express, that gives your application a defined shape: a **dependency injection container**, a **module registration system**, and a **declarative router**, instead of leaving you to hand-wire `app
Maintainers
Readme
✨ What is Silay?
Silay is a mini-framework for Node.js, built on top of Express, that gives your application a defined shape: a dependency injection container, a module registration system, and a declarative router, instead of leaving you to hand-wire app.use() and app.get() calls across a growing codebase.
Rather than replacing Express, Silay uses it as its HTTP layer and adds the structural pieces most Express apps eventually need to build by hand:
- A DI container so services, controllers, and middleware are registered once and resolved wherever they're needed.
- A module manager that registers and instantiates classes, functions, and values in a predictable, dependency-aware order.
- A declarative router that maps route definitions to controller methods, instead of scattering handlers across files.
- A small set of built-in middleware (request tracking, a standardized response helper) applied automatically to every app.
Who it's for: developers who like Express's simplicity but want a consistent way to organize controllers, services, and routes as an application grows past a handful of endpoints — without adopting a large, opinionated framework.
How it differs from plain Express: a bare Express app leaves dependency wiring, controller organization, and route/middleware composition entirely up to you. Silay layers a container and a builder API (AppBuilder) on top of Express so those concerns follow one consistent pattern across the whole app.
🚀 Features
- 🛣️ Routing — declarative route definitions mapped to controller methods, with per-route middleware.
- 💉 Dependency Injection — a container supporting classes, functions, and values, with singleton resolution and dependency graphs.
- 🧩 Modular Architecture — register controllers, services, middleware, and schemas as module items and let Silay instantiate them in the right order.
- 🔌 Middleware — global (app-level) and route-level middleware, resolved through the DI container.
- ⚡ Lightweight Core — a thin layer over Express; no hidden runtime, no code generation.
- 🏗️ Built-in Request Utilities — automatic request tracking headers and a
res.sendResponse()helper on every request. - 🧱 Extensible Error Handling — a default JSON error handler, or supply your own via the container.
📦 Installation
npm install @silay/coreSilay depends on Express 5, so it requires Node.js 18 or later.
⚡ Quick Start
const AppBuilder = require('@silay/core');
// 1. A controller — a plain class with methods that handle requests
class HelloController {
sayHello(req, res) {
res.sendResponse(200, { message: 'Hello from Silay!' });
}
}
// 2. Create the app builder
const app = new AppBuilder();
// 3. Register the controller with the DI container / module system
app.registerModuleConfigs([
{
key: 'helloController',
type: 'controller',
Class: HelloController,
dependencies: [],
},
]);
// 4. Declare routes for that controller
const helloRoutes = {
configs: {
prefix: '/hello',
controller: 'helloController',
},
routes: [
{ url: '/', httpMethod: 'get', methodName: 'sayHello' },
],
};
// 5. Build the routes and start the server
app.buildRoutes([helloRoutes]);
app.listen(3000, () => console.log('Silay app running on port 3000'));GET /hello/ now responds with:
{ "message": "Hello from Silay!", "trackingHeaders": { "userIp": "...", "userAgent": "...", "requestId": "..." } }🛣️ Routing
Routing in Silay is declarative: you describe a group of routes and the controller that serves them, and RouterManager wires them into an Express Router() under the hood.
A route module has two parts — configs (the controller and an optional URL prefix) and routes (the individual endpoints):
const userRoutes = {
configs: {
prefix: '/users',
controller: 'userController', // key registered in the DI container
},
routes: [
{ url: '/', httpMethod: 'get', methodName: 'list' },
{ url: '/:id', httpMethod: 'get', methodName: 'getById' },
{ url: '/', httpMethod: 'post', methodName: 'create' },
{ url: '/:id', httpMethod: 'put', methodName: 'update' },
{ url: '/:id', httpMethod: 'patch', methodName: 'partialUpdate' },
{ url: '/:id', httpMethod: 'delete', methodName: 'remove' },
],
};
app.buildRoutes([userRoutes]);- Supported HTTP methods — any method available on an Express
Router(get,post,put,patch,delete, etc.);httpMethoddefaults togetif omitted. - Route parameters — standard Express path parameters (
/:id) are supported, since routes are registered directly on an Express router. - Handler structure —
methodNamemust match a method on the controller instance registered undercontroller. Silay resolves the controller from the DI container and callscontroller[methodName](req, res, next), automatically forwarding thrown/rejected errors tonext(). - Route-level middleware — each route can list middleware by name (see Middleware); they run before the handler, in order.
- Multiple route modules can be passed to
buildRoutes([...])to compose a full application from separately defined route groups.
💉 Dependency Injection
Silay's DI container (DIContainer) resolves services, controllers, and middleware by name instead of requiring you to require() and wire them together manually.
Registering dependencies
The container exposes three registration methods, each accepting an optional { singleton, type }:
const container = app.getContainer();
container.registerClass('logger', Logger, { singleton: true, type: 'service' });
container.registerFunction('formatDate', (date) => date.toISOString(), { singleton: true });
container.registerValue('config', { port: 3000 }, { singleton: true });registerClass(name, Class, options)— registers a constructor. Instances of classes are instantiated throughinstantiateClass()(or automatically by the module system — see Modules), notget().registerFunction(name, fn, options)— registers a plain function as a resolvable dependency.registerValue(name, value, options)— registers a ready-made value or instance.- Singletons default to
true: once resolved, the same instance/value is returned on every subsequentget().
Resolving dependencies
container.has('logger'); // -> true
container.get('formatDate'); // -> the registered function/value
container.instantiateClass('logger', deps); // -> a class instance
container.getAllByType('service'); // -> { logger: <instance>, ... }In practice, most apps don't call the container directly — they register a module item with dependencies: [...], and the module system resolves and injects those dependencies automatically when it instantiates the class:
class DatabaseService {
constructor() { /* ... */ }
}
class UserService {
constructor(databaseService) {
this.db = databaseService;
}
}
app.registerModuleConfigs([
{ key: 'databaseService', type: 'service', Class: DatabaseService, dependencies: [] },
{ key: 'userService', type: 'service', Class: UserService, dependencies: ['databaseService'] },
]);Here, UserService receives an already-instantiated DatabaseService as its constructor argument, resolved from the container by key.
🧩 Modules
A module item is a plain object describing something that should be registered in the DI container: a controller, service, middleware, or schema. ModuleManager processes a list of these items in two phases — first registering every definition, then instantiating singletons (resolving each item's dependencies along the way).
app.registerModuleConfigs([
// A service with no dependencies
{ key: 'databaseService', type: 'service', Class: DatabaseService, dependencies: [] },
// A controller that depends on the service above
{
key: 'userController',
type: 'controller',
Class: UserController,
dependencies: ['databaseService'],
},
// A reusable value, e.g. a validation schema
{ key: 'userSchema', type: 'schema', schema: userSchemaObject },
]);Each item supports:
| Field | Description |
| -------------- | ----------------------------------------------------------------------------|
| key | Unique name used to register and resolve the item in the container. |
| type | Category of the item (controller, service, middleware, schema, etc. middleware and schema are handled specially — see below). |
| Class / fn / schema | The implementation — a constructor (Class), a function (fn, for middleware), or a value (schema). |
| dependencies | Array of other module keys to resolve and inject, in declaration order. |
| options.singleton | Whether the instance is cached and reused (defaults to true). |
Calling app.registerModuleConfigs([...]) both registers the items and immediately loads them (registerItems + loadModules), so controllers and services are ready before routes are built. Because resolution happens by key, modules can be organized and registered from separate files and combined into a single array — this is how larger applications are expected to be composed: group related services/controllers/routes per feature, then pass each group's module items and route definitions into the builder.
🔌 Middleware
Silay applies a small set of built-in middleware to every app automatically (via createApp()): static file serving from /public, JSON and URL-encoded body parsing, cookie parsing, request tracking, and the response helper. On top of that, you can register your own middleware at the global or route level.
Built-in middleware
trackingHeaders— attachesreq.trackingHeaders(userIp,userAgent,requestId) to every request.sendResponse— addsres.sendResponse(statusCode, data), which sends a JSON response merged with the current request's tracking headers.
Global middleware
// A plain function
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// Or a middleware registered through the module system, referenced by key
app.registerModuleConfigs([
{ key: 'requestLogger', type: 'middleware', fn: (req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
},
},
]);
app.use('requestLogger');Route-level middleware
Reference registered middleware by name on individual routes:
const routes = {
configs: { prefix: '/users', controller: 'userController' },
routes: [
{
url: '/:id',
httpMethod: 'get',
methodName: 'getById',
middlewares: [{ name: 'requestLogger' }],
},
],
};Route middleware runs in the order listed, before the controller's handler.
Error handling
If no custom error handler is supplied, Silay attaches a default one that logs the error and responds with a JSON error payload (including the stack trace outside of NODE_ENV=production). You can override it by registering your own handler in the container and calling withErrorHandler(name):
container.registerFunction('errorHandler', () => (err, req, res, next) => {
res.status(err.status || 500).json({ message: err.message });
});
app.withErrorHandler('errorHandler');🏗️ Architecture
AppBuilder is the entry point that ties everything together: it owns the DI container, the module manager, and the router manager, and produces the underlying Express app.
graph TD
AppBuilder --> DIContainer
AppBuilder --> ModuleManager
AppBuilder --> RouterManager
AppBuilder --> ExpressApp["Express App (createApp)"]
ModuleManager -->|registers controllers, services, middleware| DIContainer
RouterManager -->|resolves controllers & middleware| DIContainer
RouterManager -->|mounts Router| ExpressApp
ExpressApp --> BuiltIns["Built-in middleware: static, json, urlencoded, cookies, tracking, sendResponse"]
BuiltIns --> Request
RouterManager --> RequestComponent responsibilities:
AppBuilder— the public API. Registers modules, applies global middleware, builds routes, and starts the server (listen) or exposes the raw Express instance (getApp).DIContainer— a registry of classes, functions, and values, resolved by key, with singleton caching.ModuleManager— turns module item definitions into container registrations, resolving each item's declared dependencies before instantiating it.RouterManager— translates route definitions into an ExpressRouter, resolving controllers and middleware from the container for each route.createApp(app-factory) — builds the base Express app and applies the built-in middleware stack.
🎯 Design Philosophy
- Simplicity — Silay is a thin layer over Express, not a replacement runtime; there's no compiler, code generation, or hidden magic.
- Explicit registration — every controller, service, and middleware is explicitly registered with a key and its dependencies; nothing is auto-discovered from the filesystem.
- Separation of concerns — routing (
RouterManager), dependency resolution (DIContainer), and lifecycle wiring (ModuleManager) are distinct components with focused responsibilities. - Low coupling via DI — controllers and services depend on names resolved through the container, not on directly
require()-ing one another. - Extensibility — the error handler, global middleware, and module items are all pluggable through the same registration APIs used for the rest of the app.
📚 API Reference
AppBuilder (default export of @silay/core)
| Method | Description |
|---|---|
| new AppBuilder() | Creates a builder with its own DI container, module manager, and router manager. |
| registerModuleConfigs(configs: Array) | Registers and loads an array of module items (controllers, services, middleware, schemas). Returns this. |
| use(middleware: Function \| string) | Adds a global middleware — either a function or the key of a middleware registered via the module system. Returns this. |
| withErrorHandler(name: string) | Uses a container-registered factory as the app's error handler instead of the default. Returns this. |
| buildRoutes(routes: Array, expressAppInstance?) | Builds the Express app (or reuses one you supply), applies global middleware, registers all route modules, and attaches the error handler. Returns this. |
| listen(port: number, callback?: Function) | Builds routes if needed and starts the HTTP server. |
| getApp() | Returns the underlying Express app, building it first if necessary. |
| getContainer() | Returns the app's DIContainer instance. |
const app = new AppBuilder();
app.registerModuleConfigs([/* ... */]).use(myMiddleware).buildRoutes([myRoutes]);
app.listen(3000);DIContainer
| Method | Description |
|---|---|
| registerClass(name, Class, options?) | Registers a class under name. options: { singleton?, type? }. |
| registerFunction(name, fn, options?) | Registers a function under name. |
| registerValue(name, value, options?) | Registers a value/instance under name. |
| has(name) | Returns true if name is registered. |
| get(name) | Resolves a function/value registration by name. Throws if name refers to an unresolved class. |
| instantiateClass(name, dependencies) | Instantiates a registered class with the given dependencies, caching it if it's a singleton. |
| getAllByType(type) | Returns a { key: instance } map of all registrations tagged with type. |
| getSingletonInstances() | Returns the internal Map of cached singleton instances. |
ModuleManager
| Method | Description |
|---|---|
| new ModuleManager(container) | Creates a manager bound to a DIContainer. |
| registerItems(items: Array) | Queues module item definitions for loading. |
| loadModules() | Registers all queued items, then instantiates singletons (resolving dependencies). |
| getAllModulesByType(type) | Delegates to container.getAllByType(type). |
| getAllLoadedSingletonInstances() | Returns a Map of every singleton instantiated by this manager. |
| getModuleInstance(key) | Resolves a single module instance from the container by key. |
RouterManager
| Method | Description |
|---|---|
| new RouterManager(app, container) | Creates a router manager bound to an Express app and a DIContainer. |
| register(routeModule) | Queues a { configs, routes } route module for registration. |
| setRoutes() | Builds an Express Router from all queued route modules and mounts it on the app. |
Built-in middleware (require('@silay/core/lib/middlewares'))
| Export | Description |
|---|---|
| createTrackingHeaders | Attaches req.trackingHeaders (userIp, userAgent, requestId) to the request. |
| sendResponse | Adds res.sendResponse(statusCode, data) to the response. |
🛠️ Development
Silay currently has no defined npm scripts in package.json. To work on the source locally:
git clone <repository-url>
cd silay
npm installYou can link the package into a local test project with npm link to try changes against a real app.
🗺️ Roadmap
Completed
- Core
AppBuilderwith routing, DI, and module registration - Dependency injection container (classes, functions, values; singleton resolution)
- Declarative router with per-route middleware
- Built-in request tracking and response helper middleware
- Default and custom error handling
Planned
- CLI for scaffolding modules, controllers, and routes
- Formal configuration system
- Test suite and testing utilities
- Additional built-in middleware (e.g. request validation helpers)
- Expanded documentation and guides
🤝 Contributing
Contributions are welcome:
- Fork the repository.
- Create a feature branch:
git checkout -b feature/my-change. - Make your changes, keeping the existing module/DI/router patterns in mind.
- Verify the app still runs as expected against a sample project.
- Open a pull request describing the change and its motivation.
📄 License
Released under the ISC License (as specified in package.json).
⭐ Final Word
If Silay fits how you like to structure Node.js apps, give it a try in a real project, open issues for anything that doesn't behave as documented, and feel free to contribute improvements back.
npm install @silay/core