Skip to content

Commit a656d29

Browse files
authored
Implement lazy loading for middleware dependencies (#11)
* feat: implement lazy loading for middleware dependencies and update documentation * feat: increase Prometheus middleware code covergage
1 parent 72f351b commit a656d29

File tree

8 files changed

+421
-31
lines changed

8 files changed

+421
-31
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,11 +206,14 @@ Bun.serve({
206206

207207
0http-bun includes a comprehensive middleware system with built-in middlewares for common use cases:
208208

209+
> 📦 **Note**: Starting with v1.2.2, some middleware dependencies are optional. Install only what you need: `jose` (JWT), `pino` (Logger), `prom-client` (Prometheus).
210+
209211
- **[Body Parser](./lib/middleware/README.md#body-parser)** - Automatic request body parsing (JSON, form data, text)
210212
- **[CORS](./lib/middleware/README.md#cors)** - Cross-Origin Resource Sharing with flexible configuration
211213
- **[JWT Authentication](./lib/middleware/README.md#jwt-authentication)** - JSON Web Token authentication and authorization
212214
- **[Logger](./lib/middleware/README.md#logger)** - Request logging with multiple output formats
213215
- **[Rate Limiting](./lib/middleware/README.md#rate-limiting)** - Flexible rate limiting with sliding window support
216+
- **[Prometheus Metrics](./lib/middleware/README.md#prometheus-metrics)** - Export metrics for monitoring and alerting
214217

215218
### Quick Example
216219

lib/middleware/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,30 @@
22

33
0http-bun provides a comprehensive middleware system with built-in middlewares for common use cases. All middleware functions are TypeScript-ready and follow the standard middleware pattern.
44

5+
## Dependency Installation
6+
7+
⚠️ **Important**: Starting with v1.2.2, middleware dependencies are now **optional** and must be installed separately when needed. This reduces the framework's footprint and improves startup performance through lazy loading.
8+
9+
Install only the dependencies you need:
10+
11+
```bash
12+
# For JWT Authentication middleware
13+
npm install jose
14+
15+
# For Logger middleware
16+
npm install pino
17+
18+
# For Prometheus Metrics middleware
19+
npm install prom-client
20+
```
21+
22+
**Benefits of Lazy Loading:**
23+
24+
- 📦 **Smaller Bundle**: Only install what you use
25+
-**Faster Startup**: Dependencies loaded only when middleware is used
26+
- 💾 **Lower Memory**: Reduced initial memory footprint
27+
- 🔧 **Better Control**: Explicit dependency management
28+
529
## Table of Contents
630

731
- [Middleware Pattern](#middleware-pattern)
@@ -96,6 +120,8 @@ import type {
96120

97121
Automatically parses request bodies based on Content-Type header.
98122

123+
> **No additional dependencies required** - Uses Bun's built-in parsing capabilities.
124+
99125
```javascript
100126
const {createBodyParser} = require('0http-bun/lib/middleware')
101127

@@ -144,6 +170,8 @@ router.use(createBodyParser(bodyParserOptions))
144170

145171
Cross-Origin Resource Sharing middleware with flexible configuration.
146172

173+
> **No additional dependencies required** - Built-in CORS implementation.
174+
147175
```javascript
148176
const {createCORS} = require('0http-bun/lib/middleware')
149177

@@ -196,6 +224,8 @@ router.use(createCORS(corsOptions))
196224
197225
JSON Web Token authentication and authorization middleware with support for static secrets, JWKS endpoints, and API key authentication.
198226
227+
> 📦 **Required dependency**: `npm install jose`
228+
199229
#### Basic JWT with Static Secret
200230
201231
```javascript
@@ -447,6 +477,9 @@ router.get('/api/profile', (req) => {
447477
448478
Request logging middleware with customizable output formats.
449479
480+
> 📦 **Required dependency for structured logging**: `npm install pino`
481+
> ✅ **Simple logger** (`simpleLogger`) has no dependencies - uses `console.log`
482+
450483
```javascript
451484
const {createLogger, simpleLogger} = require('0http-bun/lib/middleware')
452485

@@ -509,6 +542,8 @@ router.use(createLogger(loggerOptions))
509542
510543
Comprehensive Prometheus metrics integration for monitoring and observability with built-in security and performance optimizations.
511544
545+
> 📦 **Required dependency**: `npm install prom-client`
546+
512547
```javascript
513548
import {createPrometheusIntegration} from '0http-bun/lib/middleware/prometheus'
514549

@@ -705,6 +740,8 @@ scrape_configs:
705740
706741
Configurable rate limiting middleware with multiple store options.
707742
743+
> ✅ **No additional dependencies required** - Uses built-in memory store.
744+
708745
```javascript
709746
const {createRateLimit, MemoryStore} = require('0http-bun/lib/middleware')
710747

@@ -1065,4 +1102,24 @@ router.get('/api/public/status', () => Response.json({status: 'ok'}))
10651102
router.get('/api/protected/data', (req) => Response.json({user: req.user}))
10661103
```
10671104
1105+
## Dependency Summary
1106+
1107+
For your convenience, here's a quick reference of which dependencies you need to install for each middleware:
1108+
1109+
| Middleware | Dependencies Required | Install Command |
1110+
| ----------------------- | --------------------- | ------------------------- |
1111+
| **Body Parser** | ✅ None | Built-in |
1112+
| **CORS** | ✅ None | Built-in |
1113+
| **Rate Limiting** | ✅ None | Built-in |
1114+
| **Logger** (simple) | ✅ None | Built-in |
1115+
| **Logger** (structured) | 📦 `pino` | `npm install pino` |
1116+
| **JWT Authentication** | 📦 `jose` | `npm install jose` |
1117+
| **Prometheus Metrics** | 📦 `prom-client` | `npm install prom-client` |
1118+
1119+
**Install all optional dependencies at once:**
1120+
1121+
```bash
1122+
npm install pino jose prom-client
1123+
```
1124+
10681125
This middleware stack provides a solid foundation for most web applications with security, logging, and performance features built-in.

lib/middleware/jwt-auth.js

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
1-
const {jwtVerify, createRemoteJWKSet, errors} = require('jose')
1+
// Lazy load jose to improve startup performance
2+
let joseLib = null
3+
function loadJose() {
4+
if (!joseLib) {
5+
try {
6+
joseLib = require('jose')
7+
} catch (error) {
8+
throw new Error(
9+
'jose is required for JWT middleware. Install it with: bun install jose',
10+
)
11+
}
12+
}
13+
return joseLib
14+
}
215

316
/**
417
* Creates JWT authentication middleware
@@ -60,6 +73,7 @@ function createJWTAuth(options = {}) {
6073
keyLike = jwks
6174
}
6275
} else if (jwksUri) {
76+
const {createRemoteJWKSet} = loadJose()
6377
keyLike = createRemoteJWKSet(new URL(jwksUri))
6478
} else if (typeof secret === 'function') {
6579
keyLike = secret
@@ -143,6 +157,7 @@ function createJWTAuth(options = {}) {
143157
}
144158

145159
// Verify JWT token
160+
const {jwtVerify} = loadJose()
146161
const {payload, protectedHeader} = await jwtVerify(
147162
token,
148163
keyLike,
@@ -302,16 +317,19 @@ function handleAuthError(error, handlers = {}, req) {
302317
message = 'Invalid API key'
303318
} else if (error.message === 'JWT verification not configured') {
304319
message = 'JWT verification not configured'
305-
} else if (error instanceof errors.JWTExpired) {
306-
message = 'Token expired'
307-
} else if (error instanceof errors.JWTInvalid) {
308-
message = 'Invalid token format'
309-
} else if (error instanceof errors.JWKSNoMatchingKey) {
310-
message = 'Token signature verification failed'
311-
} else if (error.message.includes('audience')) {
312-
message = 'Invalid token audience'
313-
} else if (error.message.includes('issuer')) {
314-
message = 'Invalid token issuer'
320+
} else {
321+
const {errors} = loadJose()
322+
if (error instanceof errors.JWTExpired) {
323+
message = 'Token expired'
324+
} else if (error instanceof errors.JWTInvalid) {
325+
message = 'Invalid token format'
326+
} else if (error instanceof errors.JWKSNoMatchingKey) {
327+
message = 'Token signature verification failed'
328+
} else if (error.message.includes('audience')) {
329+
message = 'Invalid token audience'
330+
} else if (error.message.includes('issuer')) {
331+
message = 'Invalid token issuer'
332+
}
315333
}
316334

317335
return new Response(JSON.stringify({error: message}), {

lib/middleware/logger.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
1-
const pino = require('pino')
21
const crypto = require('crypto')
32

3+
// Lazy load pino to improve startup performance
4+
let pino = null
5+
function loadPino() {
6+
if (!pino) {
7+
try {
8+
pino = require('pino')
9+
} catch (error) {
10+
throw new Error(
11+
'pino is required for logger middleware. Install it with: bun install pino',
12+
)
13+
}
14+
}
15+
return pino
16+
}
17+
418
/**
519
* Creates a logging middleware using Pino logger
620
* @param {Object} options - Logger configuration options
@@ -27,9 +41,10 @@ function createLogger(options = {}) {
2741
} = options
2842

2943
// Build final pino options with proper precedence
44+
const pinoLib = loadPino()
3045
const finalPinoOptions = {
3146
level: level || pinoOptions.level || process.env.LOG_LEVEL || 'info',
32-
timestamp: pino.stdTimeFunctions.isoTime,
47+
timestamp: pinoLib.stdTimeFunctions.isoTime,
3348
formatters: {
3449
level: (label) => ({level: label.toUpperCase()}),
3550
},
@@ -41,15 +56,15 @@ function createLogger(options = {}) {
4156
...(logBody && req.body ? {body: req.body} : {}),
4257
}),
4358
// Default res serializer removed to allow logResponse to handle it fully
44-
err: pino.stdSerializers.err,
59+
err: pinoLib.stdSerializers.err,
4560
// Merge in custom serializers if provided
4661
...(serializers || {}),
4762
},
4863
...pinoOptions,
4964
}
5065

5166
// Use injected logger if provided (for tests), otherwise create a new one
52-
const logger = injectedLogger || pino(finalPinoOptions)
67+
const logger = injectedLogger || pinoLib(finalPinoOptions)
5368

5469
return function loggerMiddleware(req, next) {
5570
const startTime = process.hrtime.bigint()

lib/middleware/prometheus.js

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
1-
const promClient = require('prom-client')
1+
// Lazy load prom-client to improve startup performance
2+
let promClient = null
3+
function loadPromClient() {
4+
if (!promClient) {
5+
try {
6+
promClient = require('prom-client')
7+
} catch (error) {
8+
throw new Error(
9+
'prom-client is required for Prometheus middleware. Install it with: bun install prom-client',
10+
)
11+
}
12+
}
13+
return promClient
14+
}
215

316
// Security: Limit label cardinality
417
const MAX_LABEL_VALUE_LENGTH = 100
@@ -42,39 +55,41 @@ function validateRoute(route) {
4255
* Default Prometheus metrics for HTTP requests
4356
*/
4457
function createDefaultMetrics() {
58+
const client = loadPromClient()
59+
4560
// HTTP request duration histogram
46-
const httpRequestDuration = new promClient.Histogram({
61+
const httpRequestDuration = new client.Histogram({
4762
name: 'http_request_duration_seconds',
4863
help: 'Duration of HTTP requests in seconds',
4964
labelNames: ['method', 'route', 'status_code'],
5065
buckets: [0.001, 0.005, 0.015, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 1, 2, 5, 10],
5166
})
5267

5368
// HTTP request counter
54-
const httpRequestTotal = new promClient.Counter({
69+
const httpRequestTotal = new client.Counter({
5570
name: 'http_requests_total',
5671
help: 'Total number of HTTP requests',
5772
labelNames: ['method', 'route', 'status_code'],
5873
})
5974

6075
// HTTP request size histogram
61-
const httpRequestSize = new promClient.Histogram({
76+
const httpRequestSize = new client.Histogram({
6277
name: 'http_request_size_bytes',
6378
help: 'Size of HTTP requests in bytes',
6479
labelNames: ['method', 'route'],
6580
buckets: [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000],
6681
})
6782

6883
// HTTP response size histogram
69-
const httpResponseSize = new promClient.Histogram({
84+
const httpResponseSize = new client.Histogram({
7085
name: 'http_response_size_bytes',
7186
help: 'Size of HTTP responses in bytes',
7287
labelNames: ['method', 'route', 'status_code'],
7388
buckets: [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000],
7489
})
7590

7691
// Active HTTP connections gauge
77-
const httpActiveConnections = new promClient.Gauge({
92+
const httpActiveConnections = new client.Gauge({
7893
name: 'http_active_connections',
7994
help: 'Number of active HTTP connections',
8095
})
@@ -239,7 +254,8 @@ function createPrometheusMiddleware(options = {}) {
239254

240255
// Collect default Node.js metrics
241256
if (collectDefaultMetrics) {
242-
promClient.collectDefaultMetrics({
257+
const client = loadPromClient()
258+
client.collectDefaultMetrics({
243259
timeout: 5000,
244260
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5],
245261
eventLoopMonitoringPrecision: 5,
@@ -409,7 +425,8 @@ function createPrometheusMiddleware(options = {}) {
409425
* @returns {Function} Request handler function
410426
*/
411427
function createMetricsHandler(options = {}) {
412-
const {endpoint = '/metrics', registry = promClient.register} = options
428+
const client = loadPromClient()
429+
const {endpoint = '/metrics', registry = client.register} = options
413430

414431
return async function metricsHandler(req) {
415432
const url = new URL(req.url, 'http://localhost')
@@ -446,14 +463,15 @@ function createMetricsHandler(options = {}) {
446463
function createPrometheusIntegration(options = {}) {
447464
const middleware = createPrometheusMiddleware(options)
448465
const metricsHandler = createMetricsHandler(options)
466+
const client = loadPromClient()
449467

450468
return {
451469
middleware,
452470
metricsHandler,
453471
// Expose the registry for custom metrics
454-
registry: promClient.register,
472+
registry: client.register,
455473
// Expose prom-client for creating custom metrics
456-
promClient,
474+
promClient: client,
457475
}
458476
}
459477

@@ -463,6 +481,11 @@ module.exports = {
463481
createPrometheusIntegration,
464482
createDefaultMetrics,
465483
extractRoutePattern,
466-
promClient,
467-
register: promClient.register,
484+
// Export lazy loader functions to maintain compatibility
485+
get promClient() {
486+
return loadPromClient()
487+
},
488+
get register() {
489+
return loadPromClient().register
490+
},
468491
}

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@
1111
},
1212
"dependencies": {
1313
"fast-querystring": "^1.1.2",
14-
"jose": "^6.0.11",
15-
"pino": "^9.7.0",
16-
"prom-client": "^15.1.3",
1714
"trouter": "^4.0.0"
1815
},
1916
"repository": {
@@ -33,7 +30,10 @@
3330
"bun-types": "^1.2.16",
3431
"mitata": "^1.0.34",
3532
"prettier": "^3.5.3",
36-
"typescript": "^5.8.3"
33+
"typescript": "^5.8.3",
34+
"jose": "^6.0.11",
35+
"pino": "^9.7.0",
36+
"prom-client": "^15.1.3"
3737
},
3838
"keywords": [
3939
"http",

0 commit comments

Comments
 (0)