Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,14 @@ Bun.serve({

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

> 📦 **Note**: Starting with v1.2.2, some middleware dependencies are optional. Install only what you need: `jose` (JWT), `pino` (Logger), `prom-client` (Prometheus).

- **[Body Parser](./lib/middleware/README.md#body-parser)** - Automatic request body parsing (JSON, form data, text)
- **[CORS](./lib/middleware/README.md#cors)** - Cross-Origin Resource Sharing with flexible configuration
- **[JWT Authentication](./lib/middleware/README.md#jwt-authentication)** - JSON Web Token authentication and authorization
- **[Logger](./lib/middleware/README.md#logger)** - Request logging with multiple output formats
- **[Rate Limiting](./lib/middleware/README.md#rate-limiting)** - Flexible rate limiting with sliding window support
- **[Prometheus Metrics](./lib/middleware/README.md#prometheus-metrics)** - Export metrics for monitoring and alerting

### Quick Example

Expand Down
57 changes: 57 additions & 0 deletions lib/middleware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@

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.

## Dependency Installation

⚠️ **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.

Install only the dependencies you need:

```bash
# For JWT Authentication middleware
npm install jose

# For Logger middleware
npm install pino

# For Prometheus Metrics middleware
npm install prom-client
```

**Benefits of Lazy Loading:**

- 📦 **Smaller Bundle**: Only install what you use
- ⚡ **Faster Startup**: Dependencies loaded only when middleware is used
- 💾 **Lower Memory**: Reduced initial memory footprint
- 🔧 **Better Control**: Explicit dependency management

## Table of Contents

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

Automatically parses request bodies based on Content-Type header.

> ✅ **No additional dependencies required** - Uses Bun's built-in parsing capabilities.

```javascript
const {createBodyParser} = require('0http-bun/lib/middleware')

Expand Down Expand Up @@ -144,6 +170,8 @@ router.use(createBodyParser(bodyParserOptions))

Cross-Origin Resource Sharing middleware with flexible configuration.

> ✅ **No additional dependencies required** - Built-in CORS implementation.

```javascript
const {createCORS} = require('0http-bun/lib/middleware')

Expand Down Expand Up @@ -196,6 +224,8 @@ router.use(createCORS(corsOptions))

JSON Web Token authentication and authorization middleware with support for static secrets, JWKS endpoints, and API key authentication.

> 📦 **Required dependency**: `npm install jose`

#### Basic JWT with Static Secret

```javascript
Expand Down Expand Up @@ -447,6 +477,9 @@ router.get('/api/profile', (req) => {

Request logging middleware with customizable output formats.

> 📦 **Required dependency for structured logging**: `npm install pino`
> ✅ **Simple logger** (`simpleLogger`) has no dependencies - uses `console.log`

```javascript
const {createLogger, simpleLogger} = require('0http-bun/lib/middleware')

Expand Down Expand Up @@ -509,6 +542,8 @@ router.use(createLogger(loggerOptions))

Comprehensive Prometheus metrics integration for monitoring and observability with built-in security and performance optimizations.

> 📦 **Required dependency**: `npm install prom-client`

```javascript
import {createPrometheusIntegration} from '0http-bun/lib/middleware/prometheus'

Expand Down Expand Up @@ -705,6 +740,8 @@ scrape_configs:

Configurable rate limiting middleware with multiple store options.

> ✅ **No additional dependencies required** - Uses built-in memory store.

```javascript
const {createRateLimit, MemoryStore} = require('0http-bun/lib/middleware')

Expand Down Expand Up @@ -1065,4 +1102,24 @@ router.get('/api/public/status', () => Response.json({status: 'ok'}))
router.get('/api/protected/data', (req) => Response.json({user: req.user}))
```

## Dependency Summary

For your convenience, here's a quick reference of which dependencies you need to install for each middleware:

| Middleware | Dependencies Required | Install Command |
| ----------------------- | --------------------- | ------------------------- |
| **Body Parser** | ✅ None | Built-in |
| **CORS** | ✅ None | Built-in |
| **Rate Limiting** | ✅ None | Built-in |
| **Logger** (simple) | ✅ None | Built-in |
| **Logger** (structured) | 📦 `pino` | `npm install pino` |
| **JWT Authentication** | 📦 `jose` | `npm install jose` |
| **Prometheus Metrics** | 📦 `prom-client` | `npm install prom-client` |

**Install all optional dependencies at once:**

```bash
npm install pino jose prom-client
```

This middleware stack provides a solid foundation for most web applications with security, logging, and performance features built-in.
40 changes: 29 additions & 11 deletions lib/middleware/jwt-auth.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
const {jwtVerify, createRemoteJWKSet, errors} = require('jose')
// Lazy load jose to improve startup performance
let joseLib = null
function loadJose() {
if (!joseLib) {
try {
joseLib = require('jose')
} catch (error) {
throw new Error(
'jose is required for JWT middleware. Install it with: bun install jose',
)
}
}
return joseLib
}

/**
* Creates JWT authentication middleware
Expand Down Expand Up @@ -60,6 +73,7 @@ function createJWTAuth(options = {}) {
keyLike = jwks
}
} else if (jwksUri) {
const {createRemoteJWKSet} = loadJose()
keyLike = createRemoteJWKSet(new URL(jwksUri))
} else if (typeof secret === 'function') {
keyLike = secret
Expand Down Expand Up @@ -143,6 +157,7 @@ function createJWTAuth(options = {}) {
}

// Verify JWT token
const {jwtVerify} = loadJose()
const {payload, protectedHeader} = await jwtVerify(
token,
keyLike,
Expand Down Expand Up @@ -302,16 +317,19 @@ function handleAuthError(error, handlers = {}, req) {
message = 'Invalid API key'
} else if (error.message === 'JWT verification not configured') {
message = 'JWT verification not configured'
} else if (error instanceof errors.JWTExpired) {
message = 'Token expired'
} else if (error instanceof errors.JWTInvalid) {
message = 'Invalid token format'
} else if (error instanceof errors.JWKSNoMatchingKey) {
message = 'Token signature verification failed'
} else if (error.message.includes('audience')) {
message = 'Invalid token audience'
} else if (error.message.includes('issuer')) {
message = 'Invalid token issuer'
} else {
const {errors} = loadJose()
if (error instanceof errors.JWTExpired) {
message = 'Token expired'
} else if (error instanceof errors.JWTInvalid) {
message = 'Invalid token format'
} else if (error instanceof errors.JWKSNoMatchingKey) {
message = 'Token signature verification failed'
} else if (error.message.includes('audience')) {
message = 'Invalid token audience'
} else if (error.message.includes('issuer')) {
message = 'Invalid token issuer'
}
}

return new Response(JSON.stringify({error: message}), {
Expand Down
23 changes: 19 additions & 4 deletions lib/middleware/logger.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
const pino = require('pino')
const crypto = require('crypto')

// Lazy load pino to improve startup performance
let pino = null
function loadPino() {
if (!pino) {
try {
pino = require('pino')
} catch (error) {
throw new Error(
'pino is required for logger middleware. Install it with: bun install pino',
)
}
}
return pino
}

/**
* Creates a logging middleware using Pino logger
* @param {Object} options - Logger configuration options
Expand All @@ -27,9 +41,10 @@ function createLogger(options = {}) {
} = options

// Build final pino options with proper precedence
const pinoLib = loadPino()
const finalPinoOptions = {
level: level || pinoOptions.level || process.env.LOG_LEVEL || 'info',
timestamp: pino.stdTimeFunctions.isoTime,
timestamp: pinoLib.stdTimeFunctions.isoTime,
formatters: {
level: (label) => ({level: label.toUpperCase()}),
},
Expand All @@ -41,15 +56,15 @@ function createLogger(options = {}) {
...(logBody && req.body ? {body: req.body} : {}),
}),
// Default res serializer removed to allow logResponse to handle it fully
err: pino.stdSerializers.err,
err: pinoLib.stdSerializers.err,
// Merge in custom serializers if provided
...(serializers || {}),
},
...pinoOptions,
}

// Use injected logger if provided (for tests), otherwise create a new one
const logger = injectedLogger || pino(finalPinoOptions)
const logger = injectedLogger || pinoLib(finalPinoOptions)

return function loggerMiddleware(req, next) {
const startTime = process.hrtime.bigint()
Expand Down
47 changes: 35 additions & 12 deletions lib/middleware/prometheus.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
const promClient = require('prom-client')
// Lazy load prom-client to improve startup performance
let promClient = null
function loadPromClient() {
if (!promClient) {
try {
promClient = require('prom-client')
} catch (error) {
throw new Error(
'prom-client is required for Prometheus middleware. Install it with: bun install prom-client',
)
}
}
return promClient
}

// Security: Limit label cardinality
const MAX_LABEL_VALUE_LENGTH = 100
Expand Down Expand Up @@ -42,39 +55,41 @@ function validateRoute(route) {
* Default Prometheus metrics for HTTP requests
*/
function createDefaultMetrics() {
const client = loadPromClient()

// HTTP request duration histogram
const httpRequestDuration = new promClient.Histogram({
const httpRequestDuration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.001, 0.005, 0.015, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 1, 2, 5, 10],
})

// HTTP request counter
const httpRequestTotal = new promClient.Counter({
const httpRequestTotal = new client.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code'],
})

// HTTP request size histogram
const httpRequestSize = new promClient.Histogram({
const httpRequestSize = new client.Histogram({
name: 'http_request_size_bytes',
help: 'Size of HTTP requests in bytes',
labelNames: ['method', 'route'],
buckets: [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000],
})

// HTTP response size histogram
const httpResponseSize = new promClient.Histogram({
const httpResponseSize = new client.Histogram({
name: 'http_response_size_bytes',
help: 'Size of HTTP responses in bytes',
labelNames: ['method', 'route', 'status_code'],
buckets: [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000],
})

// Active HTTP connections gauge
const httpActiveConnections = new promClient.Gauge({
const httpActiveConnections = new client.Gauge({
name: 'http_active_connections',
help: 'Number of active HTTP connections',
})
Expand Down Expand Up @@ -239,7 +254,8 @@ function createPrometheusMiddleware(options = {}) {

// Collect default Node.js metrics
if (collectDefaultMetrics) {
promClient.collectDefaultMetrics({
const client = loadPromClient()
client.collectDefaultMetrics({
timeout: 5000,
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5],
eventLoopMonitoringPrecision: 5,
Expand Down Expand Up @@ -409,7 +425,8 @@ function createPrometheusMiddleware(options = {}) {
* @returns {Function} Request handler function
*/
function createMetricsHandler(options = {}) {
const {endpoint = '/metrics', registry = promClient.register} = options
const client = loadPromClient()
const {endpoint = '/metrics', registry = client.register} = options

return async function metricsHandler(req) {
const url = new URL(req.url, 'http://localhost')
Expand Down Expand Up @@ -446,14 +463,15 @@ function createMetricsHandler(options = {}) {
function createPrometheusIntegration(options = {}) {
const middleware = createPrometheusMiddleware(options)
const metricsHandler = createMetricsHandler(options)
const client = loadPromClient()

return {
middleware,
metricsHandler,
// Expose the registry for custom metrics
registry: promClient.register,
registry: client.register,
// Expose prom-client for creating custom metrics
promClient,
promClient: client,
}
}

Expand All @@ -463,6 +481,11 @@ module.exports = {
createPrometheusIntegration,
createDefaultMetrics,
extractRoutePattern,
promClient,
register: promClient.register,
// Export lazy loader functions to maintain compatibility
get promClient() {
return loadPromClient()
},
get register() {
return loadPromClient().register
},
}
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@
},
"dependencies": {
"fast-querystring": "^1.1.2",
"jose": "^6.0.11",
"pino": "^9.7.0",
"prom-client": "^15.1.3",
"trouter": "^4.0.0"
},
"repository": {
Expand All @@ -33,7 +30,10 @@
"bun-types": "^1.2.16",
"mitata": "^1.0.34",
"prettier": "^3.5.3",
"typescript": "^5.8.3"
"typescript": "^5.8.3",
"jose": "^6.0.11",
"pino": "^9.7.0",
"prom-client": "^15.1.3"
},
"keywords": [
"http",
Expand Down
Loading