Skip to content

Commit 60e6a8d

Browse files
committed
feat(router): add route-specific middleware and improve documentation 💫
- Add comprehensive documentation for route-specific middleware usage - Add route-specific middleware support with path-based matching - Fix static file serving documentation with correct URL examples - Improve Handler function parameter organization and JSDoc comments - Refactor import path resolution using Node.js pathToFileURL - Remove deprecated parseImport method in favor of standard URL handling
1 parent de1937b commit 60e6a8d

File tree

3 files changed

+159
-27
lines changed

3 files changed

+159
-27
lines changed

README.md

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ HTTP server with file-based routing library for Deno that supports middleware an
1313
- [Dynamic Routes](#dynamic-routes)
1414
- [Supported HTTP Methods](#supported-http-methods)
1515
- [Middleware](#middleware)
16+
- [Global Middleware](#global-middleware)
17+
- [Route-Specific Middleware](#route-specific-middleware)
1618
- [Built-in Middleware](#built-in-middleware)
1719
- [CORS Middleware](#cors-middleware)
1820
- [Static File Serving](#static-file-serving)
@@ -128,6 +130,14 @@ Export any of these HTTP methods from your route files:
128130

129131
## Middleware
130132

133+
Deserve supports both global and route-specific middleware. Middleware executes in the following order:
134+
135+
1. **Route-specific middleware** - Applied to matching route patterns
136+
2. **Global middleware** - Applied to all routes
137+
3. **Route handlers** - Execute the actual route logic
138+
139+
### Global Middleware
140+
131141
Add global middleware to process requests before they reach route handlers:
132142

133143
```typescript
@@ -164,6 +174,85 @@ router.use((req: Request) => {
164174
router.serve(8000)
165175
```
166176

177+
### Route-Specific Middleware
178+
179+
Apply middleware to specific route patterns using the same `use()` method with a route path:
180+
181+
```typescript
182+
import { Router } from '@neabyte/deserve'
183+
184+
const router = new Router()
185+
186+
// Global middleware (applies to ALL routes)
187+
router.use((req: Request) => {
188+
console.log(`🌐 [GLOBAL] ${req.method} ${req.url}`)
189+
return null
190+
})
191+
192+
// Route-specific middleware
193+
router.use('/api', (req: Request) => {
194+
const token = req.headers.get('Authorization')
195+
if (!token) {
196+
return new Response('Unauthorized', { status: 401 })
197+
}
198+
console.log('✅ API route authenticated')
199+
return null
200+
})
201+
202+
router.use('/api/admin', (req: Request) => {
203+
const role = req.headers.get('X-User-Role')
204+
if (role !== 'admin') {
205+
return new Response('Forbidden', { status: 403 })
206+
}
207+
console.log('✅ Admin route authorized')
208+
return null
209+
})
210+
211+
router.use('/public', (req: Request) => {
212+
console.log('🌍 Public route accessed')
213+
return null
214+
})
215+
216+
// Start the server
217+
router.serve(8000)
218+
```
219+
220+
**Route Pattern Matching:**
221+
- `router.use('/api', middleware)` - Applies to `/api/*` routes
222+
- `router.use('/api/admin', middleware)` - Applies to `/api/admin/*` routes
223+
- `router.use('/public', middleware)` - Applies to `/public/*` routes
224+
225+
**Multiple Middleware per Route:**
226+
```typescript
227+
// Apply multiple middleware to the same route
228+
router.use('/api', authMiddleware)
229+
router.use('/api', rateLimitMiddleware)
230+
router.use('/api', corsMiddleware)
231+
```
232+
233+
**Middleware Composition:**
234+
```typescript
235+
// Compose middleware for cleaner code
236+
const apiMiddleware = (req: Request) => {
237+
// Run auth check
238+
const authResult = authMiddleware(req)
239+
if (authResult) {
240+
return authResult
241+
}
242+
243+
// Run rate limiting
244+
const rateLimitResult = rateLimitMiddleware(req)
245+
if (rateLimitResult) {
246+
return rateLimitResult
247+
}
248+
249+
// ... more middleware ...
250+
}
251+
252+
// Apply middleware to the API route
253+
router.use('/api', apiMiddleware)
254+
```
255+
167256
## Built-in Middleware
168257

169258
Deserve includes built-in middleware that can be applied using the `apply()` method:
@@ -215,10 +304,10 @@ router.static('/', {
215304
router.serve(8000)
216305
```
217306

218-
This serves files from the `public/` directory at the `/static` URL path:
219-
- `GET /static/index.html` → serves `public/index.html`
220-
- `GET /static/css/style.css` → serves `public/css/style.css`
221-
- `GET /static/js/app.js` → serves `public/js/app.js`
307+
This serves files from the `public/` directory at the root URL path:
308+
- `GET /index.html` → serves `public/index.html`
309+
- `GET /css/style.css` → serves `public/css/style.css`
310+
- `GET /js/app.js` → serves `public/js/app.js`
222311

223312
**Note:** The `urlRoot` parameter is automatically set to strip the leading `/` from the URL path, so you don't need to specify it manually.
224313

@@ -419,7 +508,30 @@ Start the HTTP server on the specified port (default: 8000).
419508

420509
##### `use(middleware: RouterMiddleware): void`
421510

422-
Add middleware to the request pipeline.
511+
Add global middleware to the request pipeline.
512+
513+
##### `use(routePath: string, middleware: RouterMiddleware): void`
514+
515+
Add route-specific middleware to the request pipeline.
516+
517+
**Parameters:**
518+
- `routePath` - Route path pattern to apply middleware to (e.g., `/api`, `/admin`)
519+
- `middleware` - Middleware function to execute for matching routes
520+
521+
**Examples:**
522+
```typescript
523+
// Global middleware
524+
router.use((req: Request) => {
525+
console.log('Global middleware')
526+
return null
527+
})
528+
529+
// Route-specific middleware
530+
router.use('/api', (req: Request) => {
531+
console.log('API middleware')
532+
return null
533+
})
534+
```
423535

424536
##### `onError(middleware: ErrorMiddleware): void`
425537

src/Handler.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,20 +89,23 @@ function findMatchingRoute(
8989

9090
/**
9191
* Request handler factory function.
92+
* @param errorMiddleware - Optional error middleware for custom error responses
9293
* @param middleware - Array of middleware functions
9394
* @param routeCache - Map of route paths to handler modules
9495
* @param routePattern - Map of URLPattern instances to route paths
96+
* @param routeSpecific - Map of route paths to route-specific middleware arrays
9597
* @param routesExt - File extension for route files
96-
* @param errorMiddleware - Optional error middleware for custom error responses
98+
* @param staticRoutes - Map of static file routes configuration
9799
* @returns Request handler function
98100
*/
99101
export function handleRequest(
102+
errorMiddleware: ErrorMiddleware | null | undefined,
100103
middleware: Array<RouterMiddleware>,
101104
routeCache: Map<string, Record<string, RouterHandler>>,
102105
routePattern: Map<URLPattern, string>,
106+
routeSpecific: Map<string, Array<RouterMiddleware>> | undefined,
103107
routesExt: string,
104-
errorMiddleware?: ErrorMiddleware | null,
105-
staticRoutes?: Map<string, ServeDirOptions>
108+
staticRoutes: Map<string, ServeDirOptions> | undefined
106109
): (req: Request) => Promise<Response> {
107110
return async (req: Request): Promise<Response> => {
108111
if (staticRoutes) {
@@ -117,11 +120,24 @@ export function handleRequest(
117120
}
118121
}
119122
}
123+
const url = new URL(req.url)
124+
if (routeSpecific) {
125+
const pathname = url.pathname
126+
for (const [routePath, middlewares] of routeSpecific) {
127+
if (pathname.startsWith(routePath)) {
128+
for (const middleware of middlewares) {
129+
const result = middleware(req)
130+
if (result) {
131+
return result
132+
}
133+
}
134+
}
135+
}
136+
}
120137
const middlewareResponse = processMiddleware(middleware, req)
121138
if (middlewareResponse) {
122139
return middlewareResponse
123140
}
124-
const url = new URL(req.url)
125141
const method = req.method
126142
const normalizedPath = normalizePath(url.pathname)
127143
const exactPath = getExactPath(normalizedPath, routesExt)

src/Router.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ServeDirOptions } from '@std/http/file-server'
22
import type { ErrorMiddleware, RouterHandler, RouterMiddleware, RouterOptions } from '@app/Types.ts'
3+
import { pathToFileURL } from 'node:url'
34
import { handleRequest } from '@app/Handler.ts'
45
import { middlewares } from '@middlewares/index.ts'
56

@@ -14,6 +15,8 @@ export class Router {
1415
private middlewarePipeline: Array<RouterMiddleware> = []
1516
/** Static file routes configuration */
1617
private staticRoutes = new Map<string, ServeDirOptions>()
18+
/** Route-specific middleware configuration */
19+
private routeSpecific = new Map<string, Array<RouterMiddleware>>()
1720
/** Cache of loaded route modules */
1821
private routeCache = new Map<string, Record<string, RouterHandler>>()
1922
/** URLPattern to route path mapping */
@@ -101,8 +104,21 @@ export class Router {
101104
* Add global middleware to the pipeline.
102105
* @param middleware - Middleware function to execute before route handlers.
103106
*/
104-
use(middleware: RouterMiddleware): void {
105-
this.middlewarePipeline.push(middleware)
107+
use(middleware: RouterMiddleware): void
108+
/**
109+
* Add route-specific middleware to the pipeline.
110+
* @param routePath - Route path pattern to apply middleware to
111+
* @param middleware - Middleware function to execute for matching routes
112+
*/
113+
use(routePath: string, middleware: RouterMiddleware): void
114+
use(routePathOrMiddleware: string | RouterMiddleware, middleware?: RouterMiddleware): void {
115+
if (typeof routePathOrMiddleware === 'string' && middleware) {
116+
const existing = this.routeSpecific.get(routePathOrMiddleware) || []
117+
existing.push(middleware)
118+
this.routeSpecific.set(routePathOrMiddleware, existing)
119+
} else if (typeof routePathOrMiddleware === 'function') {
120+
this.middlewarePipeline.push(routePathOrMiddleware)
121+
}
106122
}
107123

108124
/**
@@ -111,11 +127,12 @@ export class Router {
111127
*/
112128
private createHandler(): (req: Request) => Promise<Response> {
113129
return handleRequest(
130+
this.errorMiddleware,
114131
this.middlewarePipeline,
115132
this.routeCache,
116133
this.routePattern,
134+
this.routeSpecific,
117135
this.routesExt,
118-
this.errorMiddleware,
119136
this.staticRoutes
120137
)
121138
}
@@ -146,18 +163,6 @@ export class Router {
146163
await this.scanRoutes(this.routesDir)
147164
}
148165

149-
/**
150-
* Parse import path by removing HTTPS URLs.
151-
* @param str - Import path string
152-
* @returns Cleaned import path
153-
*/
154-
private parseImport(str: string): string {
155-
if (str.startsWith('https://')) {
156-
return str.replace(/^https:\/\/[^\/]+/, 'file://')
157-
}
158-
return str
159-
}
160-
161166
/**
162167
* Recursively scan directory for route files.
163168
* @param dir - Directory to scan
@@ -172,9 +177,8 @@ export class Router {
172177
if (entry.isDirectory) {
173178
await this.scanRoutes(fullPath, routePath)
174179
} else if (entry.name.endsWith(this.routesExt)) {
175-
const resolvedPath = import.meta.resolve(fullPath)
176-
const cleanPath = this.parseImport(resolvedPath)
177-
const module = await import(cleanPath)
180+
const fileURL = pathToFileURL(fullPath).href
181+
const module = await import(fileURL)
178182
this.routeCache.set(routePath, module)
179183
const urlPattern = this.createURLPattern(routePath)
180184
if (urlPattern) {

0 commit comments

Comments
 (0)