Skip to content

Commit

Permalink
web-server: controllers: introduce globalMiddleware, deprecate middle…
Browse files Browse the repository at this point in the history
…ware

fixes #103
  • Loading branch information
rezonant committed Jul 15, 2024
1 parent 56791af commit 87d363f
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 14 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# ⏭ vNext
- `@/web-server`
* Adds `globalMiddleware` option to `@Controller()`. This has the same semantics as `middleware` in previous releases,
but with clarified naming.
* The `middleware` option of `@Controller()` is now deprecated in favor of `globalMiddleware`.

# v3.11.0
- `@/annotations`
Expand Down
92 changes: 86 additions & 6 deletions packages/web-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,11 +349,48 @@ Modules, controllers and services all participate in dependency injection. For m

# Middleware

Alterior supports Connect middleware (as used in Express, Fastify, etc). Middleware can be connected globally or
declared as part of a route.
Alterior supports Connect-style middleware (as used in Connect, Express, Fastify, etc) as well as middleware classes
(which support dependency injection). Middleware can be connected globally or declared as part of a route.

To add middleware globally you can declare the `middleware` property on your `@WebService` or via
`WebServerModule.configure()`.
## Dependency Injection

Middleware can use dependency injection when they are declared as a middleware class. A middleware class must contain
a `handle()` function which takes the same arguments as a normal Connect-style middleware function (request, response, next).

```typescript
import { inject } from '@alterior/di';

class MyMiddleware {
private service = inject(MyService);

handle(request: IncomingMessage, response: ServerResponse, next: (err?: any) => void) {
// ...
next();
}
}
```

## Global vs Route Middleware

Alterior handles middleware differently depending on whether it is mounted as global middleware or route middleware.

Global middleware:
- Is run as part of the underlying web server engine (Express, Fastify, etc)
- Supports path prefixing
- Does *not* have access to Alterior's `WebEvent.current` API

Route middleware:
- Is run by Alterior as part of executing a route
- Does not support path prefixing
- Has full access to Alterior's `WebEvent.current` API

Within global middleware `WebEvent.current` will be `undefined`. Accessing the current `WebEvent` allows a middleware
to introspect the route that Alterior is about to run. This can be used to enable custom decorators and metadata
scenarios that otherwise would be impossible.

## Global Middleware

To add global middleware across your application you can declare the `middleware` property on your `@WebService`.

```typescript
import * as myConnectMiddleware from 'my-connect-middleware';
Expand All @@ -365,6 +402,8 @@ export class MyService {
}
```

## Path-specific Global Middleware

You can also connect middleware globally, but limit it to specific paths:

```typescript
Expand All @@ -380,7 +419,9 @@ export class MyService {
}
```

To add route-specific middleware, use the `middleware` property of the options object:
## Route Middleware

To add middleware to a specific route, use the `middleware` property of the options object:

```typescript
@Get('/foo', { middleware: [fileUpload] })
Expand All @@ -389,6 +430,25 @@ public getFoo() {
}
```

## Apply middleware to all routes

You can apply route-specific middleware just before all routes of a `@Controller()` or `@WebService()` using
`preRouteMiddleware`. Similarly you can add middleware after all route-specific middleware using
`postRouteMiddleware`.

```typescript
import fileUpload = require('express-fileupload');

@Controller('/files', {
preRouteMiddleware: [ myAuthMiddleware ]
})
export class MyController {
// ...
}
```

## Route Middleware is inherited through `@Mount()`

Middleware is inherited from parent controllers when using `@Mount()`,
you can use this to avoid repeating yourself when building more complex
services:
Expand All @@ -408,7 +468,7 @@ services:
}

@Controller('', {
middleware: [
preRouteMiddleware: [
corsExampleMiddleware({ allowOrigin: '*' })
]
})
Expand All @@ -418,6 +478,26 @@ services:
}
```

# Global Middleware within `@Controller()`

You can also connect global middleware at the `@Controller()` level which is automatically limited to the path prefix
of the controller. **Caution**: If you have multiple controllers with the same path prefix (especially common when
using prefix-less controllers, which is a recommended practice), **you may inadvertently apply middleware to more
routes than you are expecting.**. You should almost always use `preRouteMiddleware` instead (see above).

```typescript
import fileUpload = require('express-fileupload');

@Controller('/files', {
globalMiddleware: [ //
fileUpload
]
})
export class MyController {
// ...
}
```

# Uncaught Exceptions

When an exception occurs while executing a controller route method (excluding HttpExceptions), Alterior will respond
Expand Down
2 changes: 2 additions & 0 deletions packages/web-server/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export class ControllerInstance {
let middleware : MiddlewareDefinition[] = [];
if (this.options.middleware)
middleware = middleware.concat(this.options.middleware);
if (this.options.globalMiddleware)
middleware = middleware.concat(this.options.globalMiddleware);

return middleware;
}
Expand Down
23 changes: 20 additions & 3 deletions packages/web-server/src/metadata/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,35 @@ export interface ControllerOptions {
basePath? : string;

/**
* Middleware to be applied to all route methods for this controller.
* Middleware to be applied to the path prefix of this controller.
*
* CAUTION: If you use prefix-less controllers (which is recommended), using this option may be unintuitive since
* the middleware is applied *to all requests with the same prefix as the controller* regardless of what route
* methods might be in the controller.
*
* @deprecated Use globalMiddleware or preRouteMiddleware to avoid ambiguity.
*/
middleware? : MiddlewareDefinition[];

/**
* Middleware to be applied to the path prefix of this controller. Such middleware will run regardless of whether
* a specific route within this controller is actually matched.
*
* CAUTION: If you use prefix-less controllers (which is recommended), using this option may be unintuitive since
* the middleware is applied *to all requests with the same prefix as the controller* regardless of what route
* methods might be in the controller.
*/
globalMiddleware?: MiddlewareDefinition[];

/**
* Wrap execution of controller methods with these interceptors. Earlier interceptors run first.
*/
interceptors?: Interceptor[];

/**
* Connect-style middleware that should be run before route-specific middleware
* has been run.
* Middleware that should be run before route-specific middleware has been run. Since the middleware is run as
* part of route handling, it will only be run if a route matches. If you want middleware to run even if no
* route matches, use `globalMiddleware` instead.
*/
preRouteMiddleware? : (MiddlewareProvider)[];

Expand Down
10 changes: 5 additions & 5 deletions packages/web-server/src/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,7 @@ suite(describe => {
next();
}

@Controller('/abc', { middleware: [counterMiddleware] })
@Controller('/abc', { globalMiddleware: [counterMiddleware] })
class TestController {
@Get('wat')
wat() {}
Expand Down Expand Up @@ -898,7 +898,7 @@ suite(describe => {
next();
}

@Controller('/abc', { middleware: [counterMiddleware] })
@Controller('/abc', { globalMiddleware: [counterMiddleware] })
class TestController {
@Get('wat')
wat() {
Expand Down Expand Up @@ -930,7 +930,7 @@ suite(describe => {
++counter;
next();
}
@Controller('', { middleware: [counterMiddleware] })
@Controller('', { globalMiddleware: [counterMiddleware] })
class FeatureController {
@Get('wat')
get() {
Expand Down Expand Up @@ -995,7 +995,7 @@ suite(describe => {
next();
}

@Controller('/abc', { middleware: [counterMiddleware] })
@Controller('/abc', { globalMiddleware: [counterMiddleware] })
class TestController {
@Get('/wat')
wat() {}
Expand Down Expand Up @@ -1026,7 +1026,7 @@ suite(describe => {
zoom : number;
}

@Controller('', { middleware: [ counterMiddleware ]})
@Controller('', { globalMiddleware: [ counterMiddleware ]})
class ApiController {
@Post('/info', { middleware: [ bodyParser.json() ] })
getX(@Body() body : MyRequestType) {
Expand Down

0 comments on commit 87d363f

Please sign in to comment.