Skip to main content
Version: 4.0.0-preview

Logging System

Production-ready logging with 11 phases.

Overview

FeatureDescription
Log LevelsTRACE, DEBUG, INFO, WARN, ERROR, FATAL
Structured LoggingObjects with full metadata
TransportsConsole, File, Custom
Context ManagementChild loggers and context tracking
Data RedactionAutomatic sensitive data protection
Performance TrackingBuilt-in metrics
Query & ExportIn-memory buffer with search
Flow VisualizationEvent flow tracking
Error SuggestionsHelpful hints for common errors
Health MonitoringCPU, memory, system health
ExpressoTS v4 logging architecture: Logger core with level filter, context, and formatter flowing to console, file, and custom transports with redaction, query, health, and flow features

Quick Start

Basic Usage

import { provide, inject, Logger } from "@expressots/core";

@provide(MyService)
export class MyService {
constructor(@inject(Logger) private logger: Logger) {}

doSomething() {
this.logger.info("User created", { userId: 123 });
this.logger.warn("Cache miss", { key: "user:123" });
this.logger.error("Database error", new Error("Connection failed"));
}
}

Registration

Register logger in your application:

export class App extends AppExpress {
async configureServices(): Promise<void> {
// Register Logger as Singleton
this.Provider.register(Logger, Scope.Singleton);
}
}

Log Levels

Six log levels with intelligent filtering:

LevelValueUse CaseExample
TRACE0Ultra-detailed debug infoFunction entry/exit, variable values
DEBUG1Detailed diagnostic informationProcessing steps, state changes
INFO2General informationUser actions, system events
WARN3Warning messagesDeprecated API usage, potential issues
ERROR4Error messagesHandled exceptions, failed operations
FATAL5Fatal errorsUnrecoverable errors, system crashes

Using Log Levels

// All log levels
this.logger.trace("Ultra detailed debug info");
this.logger.debug("Detailed diagnostic info");
this.logger.info("General information");
this.logger.warn("Warning message");
this.logger.error("Error message");
this.logger.fatal("Fatal error");

Filtering by Level

Only logs at or above the configured level are output:

import { Logger, LogLevel } from "@expressots/core";

// Set minimum level to INFO
this.logger.configure({ level: LogLevel.INFO });

this.logger.trace("Not shown"); // Below INFO
this.logger.debug("Not shown"); // Below INFO
this.logger.info("Shown"); // INFO and above
this.logger.warn("Shown"); // Above INFO
this.logger.error("Shown"); // Above INFO

Structured Logging

Log complex data with full TypeScript support:

Basic Structured Data

this.logger.info("User created", {
userId: 123,
plan: "premium",
timestamp: new Date()
});

With Context

this.logger.info("Order processed", "OrderService", {
orderId: 456,
total: 99.99,
items: 3,
paymentMethod: "credit_card"
});

Error Objects

try {
await database.connect();
} catch (error) {
this.logger.error(
"Database connection failed",
"DatabaseService",
error as Error
);
}

Output (Development):

[ExpressoTS] 2026-01-10 10:30:45 [PID:12345] ERROR [DatabaseService] Database connection failed
├─ error: Error: Connection timeout
└─ stack:
at Database.connect (database.ts:42:15)
at DatabaseService.initialize (service.ts:18:30)

Context Management

Using withContext()

Temporarily set context for a single log:

this.logger.withContext("PaymentService").info("Processing payment");
// [ExpressoTS] ... INFO [PaymentService] Processing payment

this.logger.withContext("EmailService").debug("Sending welcome email");
// [ExpressoTS] ... DEBUG [EmailService] Sending welcome email

Child Loggers

Create a child logger with persistent context:

export class UserService {
private serviceLogger: Logger;

constructor(@inject(Logger) private logger: Logger) {
// Create child with permanent context
this.serviceLogger = logger.child("UserService");
}

createUser(data: CreateUserDto) {
this.serviceLogger.info("Creating user", { email: data.email });
this.serviceLogger.debug("Validating user data");
this.serviceLogger.info("User created successfully", { userId: 123 });
}
}

Transport System

Transports control log destinations.

Console Transport

Development Format (Pretty, Colored)

import { Logger, ConsoleTransport } from "@expressots/core";

this.logger.configure({
transports: [ConsoleTransport.forDevelopment()]
});

Output:

[ExpressoTS] 2026-01-10 10:30:45 [PID:12345] INFO [UserService] User created
├─ userId: 123
├─ email: "[email protected]"
└─ plan: "premium"

Production Format (JSON)

this.logger.configure({
transports: [ConsoleTransport.forProduction()]
});

Output:

{"timestamp":"2026-01-10T10:30:45.123Z","level":"INFO","context":"UserService","message":"User created","data":{"userId":123,"email":"[email protected]","plan":"premium"}}

File Transport

Daily Rotating Files

import { Logger, FileTransport } from "@expressots/core";

this.logger.configure({
transports: [
ConsoleTransport.forDevelopment(),
FileTransport.daily({
directory: "logs",
filename: "app" // Creates app-YYYY-MM-DD.log
})
]
});

Size-Based Rotation

FileTransport.rotating({
directory: "logs",
filename: "app.log",
maxSize: "10m", // Rotate at 10MB
maxFiles: 5 // Keep 5 files
})

Fixed Filename

FileTransport.fixed({
directory: "logs",
filename: "application.log"
})

Custom Transports

Create custom transports for databases, external services, etc.:

import { Transport, LogEntry } from "@expressots/core";

class DatabaseTransport implements Transport {
async write(entry: LogEntry): Promise<void> {
await database.logs.insert({
level: entry.level,
message: entry.message,
context: entry.context,
timestamp: entry.timestamp,
data: entry.data
});
}
}

// Use it
this.logger.configure({
transports: [
ConsoleTransport.forDevelopment(),
new DatabaseTransport()
]
});

Multiple Transports

Log to multiple destinations simultaneously:

this.logger.configure({
transports: [
ConsoleTransport.forDevelopment(),
FileTransport.daily({ directory: "logs" }),
FileTransport.fixed({
directory: "logs/errors",
filename: "errors.log",
level: LogLevel.ERROR // Only errors
}),
new DatabaseTransport(),
new CloudWatchTransport()
]
});

Configuration

Complete Configuration

import { Logger, LogLevel, ConsoleTransport, FileTransport } from "@expressots/core";

const appLogger = this.Provider.get(Logger);

appLogger.configure({
// Minimum log level
level: LogLevel.DEBUG,

// Transports (where to write logs)
transports: [
ConsoleTransport.forDevelopment(),
FileTransport.daily({ directory: "logs" })
],

// Error suggestions (Phase 8)
suggestions: {
enabled: true,
showRouteSuggestions: true,
showErrorHints: true,
maxRouteSuggestions: 3,
minSimilarityThreshold: 0.3
},

// Flow visualization (Phase 7)
flow: {
enabled: true,
showVisualization: true,
trackMemory: true,
trackNested: true
},

// Health monitoring (Phase 9)
health: {
enabled: true,
interval: 30000, // Check every 30s
memoryThreshold: 80, // Alert at 80% memory
cpuThreshold: 90, // Alert at 90% CPU
alertsEnabled: true
},

// Query & export (Phase 11)
query: {
enabled: true,
bufferSize: 1000, // Keep last 1000 logs
enableExport: true
},

// Performance tracking (Phase 5)
performance: {
enabled: true,
slowThreshold: 1000 // Warn if operation > 1s
},

// Redaction (Phase 4)
redaction: {
enabled: true,
partialRedaction: true,
revealLastChars: 4
}
});

Environment-Based Configuration

const isDevelopment = process.env.NODE_ENV !== "production";

this.logger.configure({
level: isDevelopment ? LogLevel.DEBUG : LogLevel.WARN,
transports: [
isDevelopment
? ConsoleTransport.forDevelopment()
: ConsoleTransport.forProduction(),
...(isDevelopment ? [] : [FileTransport.daily({ directory: "logs" })])
],
suggestions: { enabled: isDevelopment },
flow: { enabled: isDevelopment },
health: { enabled: !isDevelopment },
query: { enabled: isDevelopment }
});

Sensitive Data Redaction

Automatically redact sensitive data (passwords, API keys, credit cards).

Automatic Redaction

Redaction is enabled by default in production:

this.logger.info("User login", {
password: "secret123", // Automatically redacted
apiKey: "sk_live_abc123", // Automatically redacted
creditCard: "4532015112830366" // Automatically redacted
});

Output:

password: "••••••••"
apiKey: "••••••••"
creditCard: "••••••••"

Partial Redaction

Show last N characters:

import { configureGlobalRedactor } from "@expressots/core";

configureGlobalRedactor({
enabled: true,
partialRedaction: true,
revealLastChars: 4
});

this.logger.info("Payment processed", {
creditCard: "4532015112830366" // Shows last 4 digits
});
// creditCard: "••••••••••••0366"

Default Patterns

Field Patterns (40+ patterns):

  • password, passwd, pwd
  • secret, apiKey, api_key
  • token, accessToken, refreshToken
  • creditCard, cardNumber, cvv
  • ssn, socialSecurity
  • And 30+ more...

Regex Patterns (9 patterns):

  • Credit cards: /^\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}$/
  • SSN: /^\d{3}-\d{2}-\d{4}$/
  • AWS keys: /^AKIA[0-9A-Z]{16}$/
  • JWT tokens: /^eyJ[a-zA-Z0-9_-]+\./
  • Stripe keys: /^sk_live_[a-zA-Z0-9]{24,}$/
  • And more...

Custom Patterns

Add your own sensitive fields:

configureGlobalRedactor({
enabled: true,
customFieldPatterns: [
"internalCode",
"employeeId",
"companySecret"
],
customRegexPatterns: [
{
name: "custom_id",
pattern: /^CUST-\d{8}$/,
replacement: "CUST-••••••••"
}
]
});

Whitelist

Prevent redaction of specific fields:

configureGlobalRedactor({
enabled: true,
whitelist: [
"password_reset_token_expires", // Won't be redacted
"api_version" // Won't be redacted
]
});

Per-Transport Configuration

Different redaction for different transports:

this.logger.configure({
transports: [
ConsoleTransport.forDevelopment({
redaction: { enabled: false } // No redaction in dev console
}),
FileTransport.daily({
directory: "logs",
redaction: { enabled: true } // Redaction in files
})
]
});

Performance Monitoring

Track operation duration and metrics:

Basic Performance Tracking

this.logger.withPerformance("Database Query", async () => {
return await database.users.findMany();
});
// Logs: "Database Query completed in 45ms"

Manual Timing

const start = Date.now();

// Your operation
await processData();

this.logger.info("Operation completed", {
duration: Date.now() - start
});

Slow Operation Warnings

this.logger.configure({
performance: {
enabled: true,
slowThreshold: 1000 // Warn if > 1 second
}
});

// Automatically warns if slow
await this.logger.withPerformance("Heavy Operation", async () => {
return await heavyComputation();
});
// ⚠️ WARNING: "Heavy Operation completed in 2500ms (slow)"

Log Grouping

Group related logs:

this.logger.group("User Registration Flow", () => {
this.logger.info("Validating user data");
this.logger.debug("Checking email uniqueness");
this.logger.info("Creating user record");
this.logger.info("Sending welcome email");
});

Output:

┌─ User Registration Flow
│ INFO Validating user data
│ DEBUG Checking email uniqueness
│ INFO Creating user record
│ INFO Sending welcome email
└─ User Registration Flow (completed in 150ms)

Nested Groups

this.logger.group("Order Processing", () => {
this.logger.info("Validating order");

this.logger.group("Payment Processing", () => {
this.logger.debug("Checking payment method");
this.logger.info("Charging credit card");
});

this.logger.info("Order confirmed");
});

Flow Visualization

Visualize event flow:

Enable Flow Tracking

this.logger.configure({
flow: {
enabled: true,
showVisualization: true,
trackMemory: true,
trackNested: true
}
});

Automatic Visualization

When events are emitted, see the flow:

UserCreatedEvent
├── SendWelcomeEmailHandler (12ms)
├── CreateUserProfileHandler (8ms)
├── LogAnalyticsHandler (2ms)
└── NotifyAdminHandler (5ms)
Total: 27ms

Access Flow Data

const flowData = this.logger.getFlowData(requestId);
console.log(flowData.events);
console.log(flowData.handlers);
console.log(flowData.totalDuration);

Error Suggestions

Helpful suggestions for common errors:

Route Suggestions

When a 404 occurs:

❌ Route not found: GET /api/usr/profile

💡 Did you mean one of these?
- GET /api/user/profile (similarity: 0.91)
- GET /api/users/profile (similarity: 0.85)
- GET /api/user/preferences (similarity: 0.72)

Error Hints

Helpful hints for common errors:

❌ Database connection failed

💡 Common causes:
- Check DATABASE_URL environment variable
- Verify database is running
- Check network connectivity
- Verify credentials are correct

Configuration

this.logger.configure({
suggestions: {
enabled: true,
showRouteSuggestions: true,
showErrorHints: true,
maxRouteSuggestions: 3,
minSimilarityThreshold: 0.3
}
});

Health Monitoring

Continuous system health monitoring:

Enable Health Monitoring

this.logger.configure({
health: {
enabled: true,
interval: 30000, // Check every 30 seconds
memoryThreshold: 80, // Alert at 80% memory usage
cpuThreshold: 90, // Alert at 90% CPU usage
alertsEnabled: true,
logLevel: "info",
alertLogLevel: "warn"
}
});

Health Metrics

The logger automatically tracks:

  • Memory Usage: Heap used, heap total, RSS
  • CPU Usage: Average CPU load
  • Uptime: Process uptime
  • Event Loop Lag: Node.js event loop delay

Automatic Alerts:

⚠️ [Health] High memory usage: 85% (680MB / 800MB)
⚠️ [Health] High CPU usage: 92%

Manual Health Check

const health = this.logger.getHealthMetrics();

console.log(health.memory.heapUsedPercentage); // 45
console.log(health.cpu.average); // 23
console.log(health.uptime); // 3600 (seconds)

Query & Export

Query and export logs from in-memory buffer:

Enable Query System

this.logger.configure({
query: {
enabled: true,
bufferSize: 1000, // Keep last 1000 logs
enableExport: true
}
});

Query Logs

By Level

const errors = this.logger.queryLogs({
level: LogLevel.ERROR
});

By Context

const userServiceLogs = this.logger.queryLogs({
context: "UserService"
});

By Time Range

const recentLogs = this.logger.queryLogs({
startTime: Date.now() - 3600000, // Last hour
endTime: Date.now()
});

By Search Term

const searchResults = this.logger.queryLogs({
search: "authentication"
});

By Regex

const matches = this.logger.queryLogs({
regex: /user-\d+/
});

Chainable Query API

Build complex queries:

const results = this.logger.query()
.level(LogLevel.ERROR)
.context("PaymentService")
.search("timeout")
.startTime(Date.now() - 3600000)
.sort("desc")
.limit(10)
.execute();

Export to Markdown

const markdown = this.logger.exportLogsToMarkdown({
query: { level: LogLevel.ERROR },
title: "Error Logs Report",
includeStats: true,
groupBy: "level"
});

// Save to file
fs.writeFileSync("error-report.md", markdown);

Generated Markdown:

# Error Logs Report

## Statistics
- Total Logs: 156
- Errors: 45
- Warnings: 67
- Info: 44

## Errors by Context
### DatabaseService (12 errors)
- 2026-01-10 10:30:45 - Connection timeout
- 2026-01-10 10:32:15 - Query failed
...

Export Options

// Group by level
this.logger.exportLogsToMarkdown({ groupBy: "level" });

// Group by context
this.logger.exportLogsToMarkdown({ groupBy: "context" });

// Filter before export
this.logger.exportLogsToMarkdown({
query: {
level: LogLevel.ERROR,
context: "PaymentService"
}
});

Statistics

const stats = this.logger.getLogStats();

console.log(stats.total); // 1000
console.log(stats.byLevel); // { ERROR: 45, WARN: 67, INFO: 888 }
console.log(stats.byContext); // { UserService: 234, OrderService: 156, ... }
console.log(stats.oldest); // timestamp
console.log(stats.newest); // timestamp

Clear Logs

this.logger.clearLogs();

Request Logging Middleware

Log HTTP requests automatically:

import { createRequestLoggingMiddleware } from "@expressots/adapter-express";

export class App extends AppExpress {
async configureServices(): Promise<void> {
const logger = this.Provider.get(Logger);

this.Middleware.add(
createRequestLoggingMiddleware(logger, {
verbosity: "detailed", // "minimal" | "standard" | "detailed"
logBody: true, // Log request body
logHeaders: false, // Log headers
slowRequestThreshold: 1000, // Warn if request > 1s
requestIdHeader: "x-request-id",
correlationIdHeader: "x-correlation-id"
})
);
}
}

Output:

→ GET /api/users/123
├─ Request ID: abc-123-def
├─ User-Agent: Mozilla/5.0...
└─ Body: {"filter":"active"}

← 200 GET /api/users/123 (45ms)
├─ Request ID: abc-123-def
└─ Body: {"id":123,"name":"John"}

Backward Compatibility

The legacy logger API still works:

// Legacy API (still supported)
this.logger.info("Message", "ModuleName");
this.logger.warn("Warning", "ModuleName");
this.logger.error("Error", "ModuleName");
this.logger.msg("Message", "ModuleName");

// New API (recommended)
this.logger.info("Message", { userId: 123 });
this.logger.withContext("ModuleName").info("Message");

Best Practices

1. Use Appropriate Log Levels

// ✅ Good
this.logger.debug("User query parameters", { filter: "active" });
this.logger.info("User created", { userId: 123 });
this.logger.warn("Cache miss", { key: "user:123" });
this.logger.error("Database error", error);

// ❌ Bad
this.logger.info("Detailed debug info..."); // Use debug
this.logger.error("Warning message"); // Use warn

2. Include Contextual Data

// ✅ Good
this.logger.error("Payment failed", {
userId: 123,
amount: 99.99,
paymentMethod: "credit_card",
errorCode: "INSUFFICIENT_FUNDS"
});

// ❌ Bad
this.logger.error("Payment failed");

3. Use Child Loggers for Services

// ✅ Good
export class UserService {
private logger: Logger;

constructor(@inject(Logger) logger: Logger) {
this.logger = logger.child("UserService");
}

createUser() {
this.logger.info("Creating user"); // Auto-includes context
}
}

// ❌ Bad
this.logger.withContext("UserService").info(...); // Repetitive

4. Configure Per Environment

// ✅ Good
const config = process.env.NODE_ENV === "production"
? {
level: LogLevel.WARN,
transports: [
ConsoleTransport.forProduction(),
FileTransport.daily({ directory: "logs" })
],
redaction: { enabled: true },
suggestions: { enabled: false }
}
: {
level: LogLevel.DEBUG,
transports: [ConsoleTransport.forDevelopment()],
redaction: { enabled: false },
suggestions: { enabled: true }
};

this.logger.configure(config);

5. Handle Errors Properly

// ✅ Good
try {
await database.connect();
} catch (error) {
this.logger.error(
"Database connection failed",
"DatabaseService",
error as Error // Pass full error object
);
throw error;
}

// ❌ Bad
catch (error) {
this.logger.error("Error"); // Missing details
}

Performance Considerations

Memory Usage

  • Buffer Size: Default 1000 logs (~1-5MB)
  • Circular Buffer: Prevents unbounded growth
  • Production: Disable query features to save memory
// Production: Minimal memory usage
this.logger.configure({
query: { enabled: false }, // Disable buffer
health: { enabled: true }, // Keep health monitoring
flow: { enabled: false } // Disable flow tracking
});

CPU Impact

  • Redaction: < 1ms per log entry
  • Query: O(n) where n = buffer size
  • Export: O(n) where n = logs to export
  • Health Monitoring: Minimal (every 30s)

Recommendations

EnvironmentLevelTransportsQueryRedactionHealth
DevelopmentDEBUGConsole (pretty)✅ Enabled❌ Disabled❌ Disabled
StagingINFOConsole + File✅ Enabled✅ Enabled✅ Enabled
ProductionWARNFile only❌ Disabled✅ Enabled✅ Enabled

Troubleshooting

Logs Not Appearing

Check log level:

this.logger.configure({ level: LogLevel.TRACE });

Check transports:

this.logger.configure({
transports: [ConsoleTransport.forDevelopment()]
});

File Transport Not Creating Files

Check directory exists:

mkdir -p logs
chmod 755 logs

Verify configuration:

FileTransport.daily({
directory: "logs", // Relative or absolute path
filename: "app"
})

Redaction Not Working

Enable redaction:

import { configureGlobalRedactor } from "@expressots/core";

configureGlobalRedactor({ enabled: true });

Check patterns:

configureGlobalRedactor({
enabled: true,
customFieldPatterns: ["myField"] // Add custom fields
});

Performance Issues

Disable expensive features in production:

this.logger.configure({
level: LogLevel.WARN,
query: { enabled: false },
flow: { enabled: false },
suggestions: { enabled: false }
});

Memory Usage High

Reduce buffer size:

this.logger.configure({
query: {
enabled: true,
bufferSize: 100 // Smaller buffer
}
});

Or disable query entirely:

this.logger.configure({
query: { enabled: false }
});

API Reference

Logger Methods

MethodDescription
trace(message, context?, data?)Log TRACE level
debug(message, context?, data?)Log DEBUG level
info(message, context?, data?)Log INFO level
warn(message, context?, data?)Log WARN level
error(message, context?, data?)Log ERROR level
fatal(message, context?, data?)Log FATAL level
withContext(context)Set temporary context
child(context)Create child logger
configure(options)Configure logger
queryLogs(options?)Query log buffer
query()Create chainable query
getAllLogs()Get all logs
getLogStats()Get statistics
clearLogs()Clear log buffer
exportLogsToMarkdown(options?)Export to markdown
getHealthMetrics()Get health metrics
getFlowData(requestId)Get flow data

Configuration Options

interface LoggerConfig {
level?: LogLevel | string;
transports?: Transport[];
suggestions?: SuggestionsConfig;
flow?: FlowConfig;
health?: HealthConfig;
query?: QueryConfig;
performance?: PerformanceConfig;
redaction?: RedactionConfig;
}

Migration from v3

Logger is now injectable

v3:

import console from "console";
console.log("Message");

v4:

@provide(MyService)
export class MyService {
constructor(@inject(Logger) private logger: Logger) {}

doSomething() {
this.logger.info("Message");
}
}

New log levels

v3: info, warn, error, msg
v4: trace, debug, info, warn, error, fatal (+ legacy methods)

Structured logging

v3:

this.logger.info("User: " + userId + " Email: " + email);

v4:

this.logger.info("User created", { userId, email });

Comparison with Other Frameworks

FeatureExpressoTSNestJSFastify
Log Levels6 levels5 levelsCustom
Structured Logging✅ Built-in✅ Via Winston✅ Via Pino
Multiple Transports✅ Built-in✅ Via Winston✅ Via Pino
Data Redaction✅ Automatic❌ Manual❌ Manual
Query & Export✅ Built-in❌ Not available❌ Not available
Health Monitoring✅ Built-in⚠️ Via terminus⚠️ Custom
Flow Visualization✅ Built-in❌ Not available❌ Not available
Error Suggestions✅ Built-in❌ Not available❌ Not available

Testing Logger

Unit Testing

Test logger in isolation with mocked transports:

describe("LoggerService", () => {
let logger: Logger;
let mockTransport: jest.Mocked<Transport>;

beforeEach(() => {
mockTransport = {
write: jest.fn().mockResolvedValue(undefined),
} as any;

logger = new Logger();
logger.configure({
transports: [mockTransport],
});
});

it("should log info messages", () => {
logger.info("Test message", { data: 123 });

expect(mockTransport.write).toHaveBeenCalledWith(
expect.objectContaining({
level: "INFO",
message: "Test message",
})
);
});

it("should respect log level filtering", () => {
logger.configure({ level: LogLevel.WARN });

logger.debug("Debug message"); // Should not log
logger.warn("Warning message"); // Should log

expect(mockTransport.write).toHaveBeenCalledTimes(1);
});
});

Testing Redaction

describe("Data Redaction", () => {
it("should redact sensitive fields", () => {
const capture = captureLogOutput();

logger.info("User login", {
password: "secret123",
});

const log = capture.logs[0];
expect(log).toContain("[email protected]");
expect(log).not.toContain("secret123");
expect(log).toContain("••••••••");
});
});

E2E Testing

describe("Logger E2E", () => {
let app: IWebServerPublic;

beforeAll(async () => {
app = await bootstrap(App, { port: 0 });
});

afterAll(async () => {
await app.close();
});

it("should log request/response cycle", async () => {
const capture = captureLogOutput();

await fetch(`http://localhost:${await app.getPort()}/users`);

expect(capture.logs).toContainEqual(
expect.objectContaining({ context: "RequestLogger" })
);
});
});

Support the Project

ExpressoTS is MIT-licensed open source. See the support guide to contribute.