@structrix/structrix
v1.0.0
Published
A robust, scalable base microservice framework for Node.js
Readme
Structrix
A robust, scalable microservice framework for Node.js built on top of Express. Structrix provides a structured, opinionated foundation for building production-ready microservices with built-in support for authentication, caching, rate limiting, metrics, logging, retries, and database integrations.
Table of Contents
- Features
- Installation
- Quick Start
- Architecture
- Core Concepts
- Configuration
- Middleware
- Utilities
- Database
- Environment Variables
- Docker
- API Endpoints
Features
- 🏗️ Modular architecture — register independent modules (service + controller + routes) at runtime
- 🔐 Authentication — JWT, API key, HTTP Basic, and OAuth2 strategies out of the box
- 🗄️ Caching — in-memory cache with TTL, tag-based invalidation, and stale-while-revalidate
- ⚡ Rate limiting — token-bucket algorithm, per-key limits, and optional Redis backend
- 📊 Metrics — Prometheus-compatible metrics via
prom-clientwith a built-in/metricsendpoint - 📝 Structured logging — Winston-powered logging with daily log rotation and JSON/text formats
- 🔁 Retry handling — exponential backoff with configurable retryable error types
- 🗃️ Database abstraction — pluggable providers for PostgreSQL, MySQL, and MongoDB
- 🛡️ Security — Helmet headers, CORS, compression, and request-ID tracing built in
- 🐳 Docker ready — includes
Dockerfileanddocker-compose.yml - 💪 TypeScript first — fully typed with strict interfaces throughout
Installation
npm install structrixOr clone and build from source:
git clone https://github.com/structrix/structrix.git
cd structrix
npm install
npm run buildQuick Start
1. Create a Service
Extend BaseService and implement the two abstract lifecycle hooks:
import { BaseService, ServiceOptions } from "structrix";
export class UserService extends BaseService {
constructor(options: ServiceOptions) {
super(options);
}
protected async onInitialize(): Promise<void> {
this.logger.info("UserService initialized");
}
protected async onShutdown(): Promise<void> {
this.logger.info("UserService shutting down");
}
async getUsers() {
return this.withCache("users:all", async () => {
// fetch from DB ...
return [{ id: "1", name: "Alice" }];
});
}
}2. Create a Controller
Extend BaseController, set the route prefix, and implement initializeRoutes:
import { BaseController } from "structrix";
import { Request, Response } from "express";
import { UserService } from "./user.service";
export class UserController extends BaseController {
constructor(service: UserService) {
super("/users", service);
}
public initializeRoutes(): void {
this.router.get("/", this.getUsers.bind(this));
}
private async getUsers(req: Request, res: Response): Promise<void> {
await this.withErrorHandler(req, res, async () => {
const users = await (this.service as UserService).getUsers();
this.sendSuccess(res, users);
});
}
}3. Create Routes
Extend BaseRoutes and use createRoute to declaratively define your routes with optional auth, caching, and rate limiting:
import { BaseRoutes } from "structrix";
import { UserController } from "./user.controller";
export class UserRoutes extends BaseRoutes {
constructor(controller: UserController) {
super(controller);
}
protected configureRoutes(): void {
this.createRoute({
path: "/",
method: "get",
handler: "getUsers",
auth: true,
cache: { ttl: 60 },
rateLimit: true
});
}
}4. Boot the Application
import Structrix from "structrix";
import { UserService } from "./user.service";
import { UserController } from "./user.controller";
import { UserRoutes } from "./user.routes";
const app = new Structrix({
name: "my-service",
version: "1.0.0",
port: 3000
});
app.registerModule({
name: "users",
service: UserService,
controller: UserController,
routes: UserRoutes,
config: {
cache: { enabled: true, ttl: 300 }
}
});
app.start();Architecture
src/
├── index.ts # Structrix app entry point & re-exports
├── types/
│ └── global.types.ts # Shared interfaces (ServiceConfig, HealthStatus, etc.)
├── core/
│ ├── base.service.ts # Abstract BaseService
│ ├── base.controller.ts # Abstract BaseController
│ ├── base.routes.ts # Abstract BaseRoutes
│ ├── base.types.ts # Core interfaces (IService, IController, etc.)
│ ├── base.repositories.ts # Abstract BaseRepository
│ ├── database.providers.ts # BaseDatabaseProvider & QueryOptions
│ └── service.factory.ts # ServiceFactory singleton
├── config/
│ ├── app.config.ts # AppConfiguration & extended config interfaces
│ └── database.config.ts # Postgres, MongoDB, MySQL providers & repositories
├── middleware/
│ ├── auth.middleware.ts # JWT / API key / Basic / OAuth2 auth
│ ├── cache.middleware.ts # Response caching & tag invalidation
│ ├── error.middleware.ts # Global error & 404 handlers
│ ├── logging.middleware.ts # Request/response logging
│ └── validation.middleware.ts # Joi schema validation for body/query/params
└── utils/
├── cacheManager.ts # In-memory CacheManager (node-cache)
├── logger.ts # Winston logger wrapper
├── metrics.ts # Prometheus MetricsCollector
├── rateLimiter.ts # Token-bucket RateLimiter
├── retryHandler.ts # Exponential-backoff RetryHandler
└── validator.ts # Joi-based Validator utilityCore Concepts
Structrix (App)
The top-level class that bootstraps Express, registers global middleware, and manages modules.
const app = new Structrix(config?: Partial<ServiceConfig>);
await app.registerModule(moduleDef); // register a module
await app.start(); // start listening
app.getApp(); // access the Express instance
app.getConfig(); // access resolved configBuilt-in endpoints registered automatically:
| Endpoint | Description |
| -------------- | ---------------------------------------- |
| GET /health | Health status of all registered services |
| GET /metrics | Prometheus metrics (when enabled) |
BaseService
Abstract base class for all services. Provides lifecycle management, logging, metrics, caching, rate limiting, and retry logic.
abstract class BaseService extends EventEmitter implements IService {
// Lifecycle
async initialize(): Promise<void>;
async shutdown(): Promise<void>;
async healthCheck(): Promise<boolean>;
async getHealthStatus(): Promise<HealthStatus>;
// Helpers
async withCache<T>(
key: string,
fn: () => Promise<T>,
ttl?: number
): Promise<T>;
async withRetry<T>(fn: () => Promise<T>, context?: string): Promise<T>;
async withRateLimit<T>(fn: () => Promise<T>, key?: string): Promise<T>;
// Abstract — implement in your subclass
protected abstract onInitialize(): Promise<void>;
protected abstract onShutdown(): Promise<void>;
}Properties automatically initialized from config:
| Property | Description |
| -------------- | ------------------------------------- |
| logger | Winston-based Logger instance |
| metrics | Prometheus MetricsCollector |
| cache | In-memory CacheManager (if enabled) |
| rateLimiter | Token-bucket RateLimiter (if enabled) |
| retryHandler | Exponential-backoff RetryHandler |
BaseController
Abstract base class for controllers. Wires up middleware, response helpers, and pagination utilities.
abstract class BaseController implements IController {
path: string;
router: Router;
service: BaseService;
// Response helpers
protected sendSuccess<T>(
res,
data?,
message?,
pagination?,
statusCode?
): Response;
protected sendError(res, error, statusCode?, details?): Response;
protected handleError(res, error): Response;
// Pagination helpers
protected getPagination(req): { page: number; limit: number };
protected getPaginationInfo(total, page, limit): PaginationInfo;
// Error-catching wrapper
protected async withErrorHandler(
req,
res,
handler: () => Promise<void>
): Promise<void>;
}BaseRoutes
Abstract base class for route configuration. Supports declarative route definitions with built-in auth, cache, and rate-limit middleware injection.
abstract class BaseRoutes {
protected abstract configureRoutes(): void;
protected createRoute(config: RouteConfig): void;
protected groupRoutes(prefix: string, routes: RouteConfig[]): void;
public getRouter(): Router;
}
interface RouteConfig {
path: string;
method: "get" | "post" | "put" | "patch" | "delete";
handler: string; // method name on your controller
middleware?: any[];
auth?: boolean;
rateLimit?: boolean;
cache?: boolean | { ttl: number };
}ServiceFactory
Singleton that manages the lifecycle and registry of all modules.
const factory = ServiceFactory.getInstance();
await factory.createModule(moduleDef, globalConfig);
factory.getService<T>("users"); // retrieve a service by name
factory.getAllRoutes(); // all registered BaseRoutes
await factory.shutdownAll(); // graceful shutdown
await factory.getHealthStatuses(); // health of all servicesConfiguration
ServiceConfig is the core configuration interface. All fields except name, version, environment, and port are optional.
interface ServiceConfig {
name: string;
version: string;
environment: "development" | "production" | "test";
port: number;
host?: string;
timeout?: number;
maxRetries?: number;
rateLimit?: {
enabled: boolean;
requestsPerMinute: number;
maxConcurrent?: number;
};
cache?: {
enabled: boolean;
ttl: number; // seconds
checkPeriod?: number; // seconds
};
logging?: {
level: "error" | "warn" | "info" | "debug";
format: "json" | "text";
output: "console" | "file" | "both";
};
metrics?: {
enabled: boolean;
collectDefaultMetrics?: boolean;
prefix?: string;
};
auth?: {
enabled: boolean;
type: "jwt" | "apikey" | "basic" | "none";
jwtSecret?: string;
tokenExpiry?: string;
};
}Middleware
Authentication
AuthMiddleware supports four strategies. Configure via AuthConfig:
| Strategy | Description |
| -------- | --------------------------------------------------------- |
| jwt | Bearer token in Authorization header, signed with HS256 |
| apikey | Key in a configurable header (default: x-api-key) |
| basic | HTTP Basic auth with a custom validator function |
| oauth2 | Bearer token validated against an external token endpoint |
// Protect a route — roles and permissions are optional
router.get(
"/admin",
authMiddleware.authenticate({ roles: ["admin"] }),
handler
);
// Generate a JWT token
const token = authMiddleware.generateToken(user);
// Hash & verify passwords (bcrypt)
const hash = await authMiddleware.hashPassword("secret");
const valid = await authMiddleware.verifyPassword("secret", hash);Caching
CacheMiddleware wraps responses in an in-memory cache. Cache keys are derived from method, path, query parameters, and optionally user ID and headers.
// Cache responses for 60 seconds
router.get("/items", cacheMiddleware.cache({ ttl: 60 }), handler);
// Invalidate cache entries matching a pattern after a write
router.post("/items", cacheMiddleware.invalidateCache("/items*"), handler);
// Serve stale content while revalidating in the background
router.get(
"/feed",
cacheMiddleware.staleWhileRevalidate({ ttl: 30, staleTtl: 120 }),
handler
);
// Tag-based invalidation
router.get("/posts", cacheMiddleware.cacheWithTags(["posts"]), handler);
router.delete(
"/posts/:id",
cacheMiddleware.invalidateByTags(["posts"]),
handler
);Cache headers added to responses:
| Header | Value |
| --------------- | ------------------------- |
| X-Cache | HIT, MISS, or STALE |
| X-Cache-Key | MD5 hash of the cache key |
| Cache-Control | public, max-age=<ttl> |
Rate Limiting
Token-bucket rate limiting, configurable per key (IP by default). Supports a Redis backend for distributed environments.
// Express middleware — limits by IP
router.use(rateLimiter.middleware());
// Custom key (e.g. authenticated user ID)
router.use(rateLimiter.middleware((req) => req.user?.id ?? req.ip));
// Programmatic acquire (throws if limit exceeded)
await rateLimiter.acquire("user:123");
// Wait for a token (up to maxWaitMs)
await rateLimiter.acquireWithWait("user:123", 5000);
// Redis-backed distributed store
const store = RateLimiter.createRedisStore(redisClient);
const limiter = new RateLimiter({ requestsPerMinute: 100, store });Response headers:
| Header | Description |
| ----------------------- | ----------------------------- |
| X-RateLimit-Limit | Total tokens per window |
| X-RateLimit-Remaining | Remaining tokens |
| X-RateLimit-Reset | Unix timestamp of next refill |
Logging
LoggingMiddleware logs every request and response with timing, request ID, and optional body capture.
Configured via ServiceConfig.logging:
| Option | Values | Default |
| -------- | -------------------------------- | --------- |
| level | error, warn, info, debug | info |
| format | json, text | text |
| output | console, file, both | console |
Log files rotate daily, kept for 14 days at up to 20 MB per file (in logs/).
Validation
ValidationMiddleware validates request data against Joi schemas and returns structured 400 errors.
import Joi from "joi";
const schema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required()
});
router.post("/users", validationMiddleware.validateBody(schema), handler);
// Also available:
validationMiddleware.validateQuery(schema);
validationMiddleware.validateParams(schema);Error Handling
ErrorMiddleware provides a global error handler and a 404 catcher. Register it last on the Express app.
All unhandled errors are returned as:
{
"success": false,
"error": "Error message",
"metadata": {
"timestamp": "2026-01-01T00:00:00.000Z",
"requestId": "uuid"
}
}Utilities
Logger
Winston-based structured logger with console and/or rotating file transports.
const logger = new Logger({ level: "info", format: "json", output: "both" });
logger.info("User created", { userId: "123" });
logger.warn("Slow query detected", { duration: 450 });
logger.error("Database connection failed", { error });
logger.debug("Cache miss", { key: "users:all" });MetricsCollector
Prometheus-compatible metrics using prom-client. Registers a set of built-in HTTP, cache, database, queue, and error metrics automatically.
// Increment a counter
metrics.increment("http_requests_total", {
method: "GET",
path: "/users",
status: "200"
});
// Set a gauge
metrics.setGauge("queue_size", 42, { queue_name: "emails" });
// Observe a histogram
metrics.observe("http_request_duration_seconds", 0.123, {
method: "GET",
path: "/users"
});
// Time an async function
const result = await metrics.time(
"database_query_duration_seconds",
async () => {
return db.query("SELECT * FROM users");
},
{ query_type: "select", collection: "users" }
);
// Get Prometheus text output
const output = await metrics.getMetrics();Built-in metrics (prefixed by ServiceConfig.metrics.prefix):
| Metric | Type | Description |
| ---------------------------------- | --------- | --------------------- |
| _http_requests_total | Counter | Total HTTP requests |
| _http_request_duration_seconds | Histogram | Request duration |
| _http_requests_active | Gauge | Active requests |
| _errors_total | Counter | Total errors |
| _cache_operations_total | Counter | Cache hits/misses |
| _database_query_duration_seconds | Histogram | DB query duration |
| _queue_size | Gauge | Current queue depth |
| _queue_processed_total | Counter | Processed queue items |
RateLimiter
Token-bucket rate limiter with in-memory or Redis-backed store.
const limiter = new RateLimiter({
requestsPerMinute: 60,
maxConcurrent: 10
});
const info = await limiter.acquire("user:123");
// info.remaining — tokens left
// info.resetTime — epoch ms when bucket refills
await limiter.reset("user:123"); // reset a specific key
await limiter.resetAll(); // reset all (memory store only)RetryHandler
Exponential backoff with configurable max delay and retryable error types.
const retryHandler = new RetryHandler({
maxRetries: 3,
backoffFactor: 2,
initialDelay: 500, // ms
maxDelay: 10000 // ms
});
const result = await retryHandler.execute(
() => externalApi.fetchData(),
"fetchData" // context label for logs
);Retry delays: 500ms → 1000ms → 2000ms (with backoffFactor: 2, initialDelay: 500).
Network errors (ECONNRESET, ETIMEDOUT) are always retried. Additional retryable error classes can be passed via retryableErrors.
CacheManager
Thin wrapper over node-cache with an async interface and set membership helpers for tag-based invalidation.
const cache = new CacheManager({ ttl: 300, checkPeriod: 60 });
await cache.set("users:all", data, 60);
const data = await cache.get<User[]>("users:all");
await cache.del("users:all");
// Set membership (used by CacheMiddleware for tag invalidation)
await cache.sadd("tag:users", "users:all");
const keys = await cache.smembers("tag:users");
await cache.close();Validator
Joi-based validator utility.
const validator = new Validator();
const schema = Joi.object({ name: Joi.string().required() });
const isValid = await validator.validate({ name: "Alice" }, schema);
const errors = validator.getErrors();Database
Providers
Three database providers are available. Each extends BaseDatabaseProvider.
import { DatabaseFactory } from './config/database.config';
const provider = DatabaseFactory.createProvider({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'user',
password: 'pass',
database: 'mydb'
});
await provider.connect();
const conn = provider.getConnection();
const client = conn.getClient(); // raw pg.Pool / mongoose / mysql2 pool
// Transactions
await conn.transaction(async (session) => {
await session.query('INSERT INTO ...', [...]);
});
await provider.healthCheck(); // true / false
await provider.disconnect();| Type | Package | Notes |
| ---------- | ---------- | --------------------------------- |
| postgres | pg | Connection pool via pg.Pool |
| mongodb | mongoose | Replica set & auth source support |
| mysql | mysql2 | Connection pool |
The DatabaseManager singleton manages multiple named providers:
import { DatabaseManager } from "./config/database.config";
const db = DatabaseManager.getInstance();
await db.initializeDefault();
await db.registerProvider("analytics", analyticsDbConfig);
const primary = db.getProvider();
const analytics = db.getProvider("analytics");
await db.healthCheckAll();
await db.disconnectAll();Repositories
Extend BaseRepository<T> to build type-safe data access layers. MongoRepository and PostgresRepository implementations are provided.
// MongoDB
class UserMongoRepository extends MongoRepository<User> {
constructor(provider: MongoDBProvider) {
super(provider, "User", userSchema);
}
}
const repo = new UserMongoRepository(mongoProvider);
const user = await repo.findById("abc123");
const users = await repo.findAll({ sort: { createdAt: -1 }, limit: 20 });
const created = await repo.create({
name: "Alice",
email: "[email protected]"
});
await repo.update("abc123", { name: "Alicia" });
await repo.delete("abc123");QueryOptions:
| Option | Description |
| ------------- | --------------------------------------- |
| limit | Max records to return |
| offset | Records to skip |
| sort | { field: 1 \| -1 } sort order |
| select | Fields to include |
| populate | Relations to populate (MongoDB) |
| lean | Return plain objects (no model methods) |
| transaction | Session/client to use for transaction |
Environment Variables
| Variable | Default | Description |
| ----------------------- | ------------- | -------------------------------------- |
| PORT | 3000 | HTTP port |
| HOST | 0.0.0.0 | Bind address |
| NODE_ENV | development | Environment |
| TIMEOUT | 30000 | Request timeout (ms) |
| MAX_RETRIES | 3 | Default max retries |
| LOG_LEVEL | info | error | warn | info | debug |
| LOG_FORMAT | text | json | text |
| LOG_OUTPUT | console | console | file | both |
| RATE_LIMIT_ENABLED | false | Enable rate limiting |
| RATE_LIMIT_REQUESTS | 60 | Requests per minute |
| RATE_LIMIT_CONCURRENT | 10 | Max concurrent requests |
| CACHE_ENABLED | false | Enable response caching |
| CACHE_TTL | 300 | Cache TTL in seconds |
| CACHE_CHECK_PERIOD | 60 | Cache cleanup interval (seconds) |
| METRICS_ENABLED | false | Enable Prometheus metrics |
| METRICS_PREFIX | structrix | Metric name prefix |
| AUTH_ENABLED | false | Enable authentication |
| AUTH_TYPE | jwt | jwt | apikey | basic | none |
| JWT_SECRET | — | Secret key for JWT signing |
| TOKEN_EXPIRY | 24h | JWT expiry (ms format string) |
| ALLOWED_ORIGINS | * | Comma-separated CORS origins |
Copy .env.example to .env to get started:
cp .env.example .envDocker
Build and run with Docker Compose (includes the app service):
# Build image
npm run docker:build
# Start services
npm run docker:up
# Stop services
npm run docker:downOr with plain Docker:
docker build -t structrix .
docker run -p 3000:3000 --env-file .env structrixAPI Endpoints
All module routes are mounted under /api. The exact sub-paths depend on the path you set in your BaseController constructor.
| Endpoint | Method | Description |
| ---------- | ------ | ----------------------------------------- |
| /health | GET | Service health status (all modules) |
| /metrics | GET | Prometheus metrics (when metrics enabled) |
| /api/** | * | Module routes (registered dynamically) |
License
MIT
