Skip to content

Commit

Permalink
Merge pull request #9 from yldrmzffr/middleware
Browse files Browse the repository at this point in the history
feat(middleware): add support for function-based middleware
  • Loading branch information
yldrmzffr authored May 5, 2024
2 parents 3c05cc4 + 2e3f147 commit 984e555
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 7 deletions.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,59 @@ Inside the `login` method, you can implement the necessary logic to handle the i

The server is started and set to listen on port 8080 using the `listen` method of the `MuzuServer` instance.

## Middleware

Muzu supports middleware functions that can be applied to routes to execute specific logic before the route handler. Middleware functions can be used for tasks such as logging, authentication, and data validation.
`@Middleware` decorator can be used to apply middleware functions to specific routes. Let's see an example:

```typescript
import {MuzuServer, Request, Response} from 'muzu';
import {HttpException} from "./http.exception";

// Create a new instance of the MuzuServer
const app = new MuzuServer();
const {Post, Controller, Middleware} = app;

// Create a Middleware Function
function LoggerMiddleware(req: Request) {
console.log(`Request Received: ${req.url}`);
}

// Create a Middleware Function
function AuthMiddleware(req: Request) {
const {token} = req.headers;

if (!token) {
throw new HttpException('Unauthorized', 401);
}

const user = getUserFromToken(token);

// You can attact the custom data to the request object
req.user = user;
}

@Controller('auth')
class TestController {

@Post('login')
@Middleware(AuthMiddleware, LoggerMiddleware) // Apply Middleware to the 'login' route
login(req: Request, res: Response) {
const {user} = req; // Access the objects attached by the middleware

// Implement your login logic here

return {
status: true
};
}

}

// Start the server and listen on port 8080
app.listen(8080);
```

## Exception Handling

Muzu provides a comprehensive exception handling mechanism. You can create custom exception classes by extending the `HttpException` class and utilize them within your application. Let's see an example:
Expand Down
3 changes: 2 additions & 1 deletion lib/controller/controller-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class ControllerFactory {
const routeHandler = target.prototype[property];
const method = Reflect.getMetadata('method', routeHandler);
const url = Reflect.getMetadata('url', routeHandler);

const middlewares = Reflect.getMetadata('middlewares', routeHandler);
if (!method || !url) return;

const fullPath = pathLib.join(path, url);
Expand All @@ -25,6 +25,7 @@ export class ControllerFactory {
method,
url: fullPath,
handler: routeHandler.bind(target.prototype),
middlewares,
} as Route;
});

Expand Down
15 changes: 12 additions & 3 deletions lib/handlers/request-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {NotFoundException} from '../exceptions/not-found.exception';
import {getRequestBody, parseQueryParams} from '../utils';
import {MuzuException} from '../exceptions/muzu.exception';
import {BadRequestException} from '../exceptions/bad-request.exception';

export class RequestHandler {
private routeManager: RouteManager;

Expand All @@ -29,7 +30,7 @@ export class RequestHandler {

req.params = parseQueryParams(url!);

const route = await this.routeManager.find(path, method);
const route = this.routeManager.find(path, method);

if (!route) {
throw new NotFoundException(`Route ${method} ${path} not found`, {
Expand All @@ -39,14 +40,22 @@ export class RequestHandler {
}

try {
const body = await getRequestBody(req);
req.body = body;
req.body = await getRequestBody(req);
} catch (error) {
console.log('🚨 Error parsing body', error);
const err = error as MuzuException;
throw new BadRequestException('Error parsing body', err.details);
}

if (route.middlewares) {
for (const middleware of route.middlewares) {
const result = middleware(req, res);
if (result instanceof Promise) {
await result;
}
}
}

let result: Object = route.handler(req, res);

if (result instanceof Promise) {
Expand Down
5 changes: 5 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ControllerFactory,
} from './controller/controller-factory';
import {MethodDecorator, MethodFactory} from './methods/method-factory';
import {MiddlewareDecorator, MiddlewareFactory} from './middleware/middleware';

export {Request, Response, RouteHandler, HttpException};

Expand All @@ -20,6 +21,7 @@ export class MuzuServer {
public readonly routeManager: RouteManager;
public readonly requestHandler: RequestHandler;
public readonly Controller: ControllerDecorator;
public readonly Middleware: MiddlewareDecorator;
public readonly Get: MethodDecorator;
public readonly Post: MethodDecorator;
public readonly Delete: MethodDecorator;
Expand All @@ -33,6 +35,9 @@ export class MuzuServer {
this.Controller = new ControllerFactory(this.routeManager).Controller;
this.requestHandler = new RequestHandler(this.routeManager);

const middleware = new MiddlewareFactory();
this.Middleware = middleware.Middleware;

this.server = createServer(
this.requestHandler.handleRequest.bind(this.requestHandler)
);
Expand Down
2 changes: 2 additions & 0 deletions lib/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ export interface Route {
method: string;
url: string;
handler: RouteHandler;
middlewares?: Function[];
}

export interface Request extends IncomingMessage {
params?: Record<string, string>;
body?: Record<string, string>;
[key: string]: any;

Check warning on line 14 in lib/interfaces/index.ts

View workflow job for this annotation

GitHub Actions / publish-npm

Unexpected any. Specify a different type
}
19 changes: 19 additions & 0 deletions lib/middleware/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export type Middleware = (
target: any,

Check warning on line 2 in lib/middleware/middleware.ts

View workflow job for this annotation

GitHub Actions / publish-npm

Unexpected any. Specify a different type
propertyKey: string,
descriptor: PropertyDescriptor
) => PropertyDescriptor;

export type MiddlewareDecorator = (...middlewares: Function[]) => Middleware;
export class MiddlewareFactory {
Middleware = (...middlewares: Function[]): Middleware => {
return (
target: any,

Check warning on line 11 in lib/middleware/middleware.ts

View workflow job for this annotation

GitHub Actions / publish-npm

Unexpected any. Specify a different type
propertyKey: string,
descriptor: PropertyDescriptor
) => {
Reflect.defineMetadata('middlewares', middlewares, target[propertyKey]);
return descriptor;
};
};
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "muzu",
"version": "0.1.0",
"version": "0.1.1",
"description": "",
"main": "build/lib/index.js",
"types": "build/lib/index.d.ts",
Expand Down
126 changes: 126 additions & 0 deletions test/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import {MuzuServer, Request, HttpException} from '../lib';
import * as request from 'supertest';

const muzuServer = new MuzuServer();
const {Controller, Get, Middleware} = muzuServer;

function Logger() {
console.log('Logger Middleware');
}

function Auth(req: Request) {
req.user = {name: 'Muzu User'};
}

function AddId(req: Request) {
req.userId = '2342';
}

function Admin() {
throw new HttpException(403, 'Unauthorized');
}

async function AsyncLogger(): Promise<void> {
return new Promise(resolve => {
setTimeout(() => {
console.log('Async Logger Middleware');
resolve();
}, 300);
});
}

@Controller('/api')
class TestController {
@Get('/hello')
@Middleware(Logger)
hello() {
return {message: 'Get Method Called'};
}

@Get('/hello-auth')
@Middleware(Auth)
helloAuth(req: Request) {
return {message: `Hello ${req.user.name}`};
}

@Get('/hello-admin')
@Middleware(Admin)
helloAdmin() {
return {message: 'Hello Admin'};
}

@Get('/hello-auth-id')
@Middleware(Auth, AddId)
helloAuthId(req: Request) {
const {user, userId} = req;

return {message: `Hello ${user.name}`, userId};
}

@Get('/hello-async-logger')
@Middleware(AsyncLogger)
helloAsyncLogger() {
return {message: 'Hello Async Logger'};
}
}

const port = 3001;
muzuServer.listen(port);

describe('MuzuServer', () => {
it('should return 200 on GET /api/hello', async () => {
const res = await request(muzuServer.server).get('/api/hello');
expect(res.status).toBe(200);
expect(res.body).toEqual(
expect.objectContaining({
message: 'Get Method Called',
})
);
});

it('should return 200 on GET /api/hello-auth', async () => {
const res = await request(muzuServer.server).get('/api/hello-auth');
expect(res.status).toBe(200);
expect(res.body).toEqual(
expect.objectContaining({
message: 'Hello Muzu User',
})
);
});

it('should return 403 on GET /api/hello-admin', async () => {
const res = await request(muzuServer.server).get('/api/hello-admin');
expect(res.status).toBe(403);
expect(res.body).toEqual(
expect.objectContaining({
status: 403,
message: 'Unauthorized',
})
);
});

it('should return 200 on GET /api/hello-auth-id', async () => {
const res = await request(muzuServer.server).get('/api/hello-auth-id');
expect(res.status).toBe(200);
expect(res.body).toEqual(
expect.objectContaining({
userId: '2342',
message: 'Hello Muzu User',
})
);
});

it('should return 200 on GET /api/hello-async-logger', async () => {
const res = await request(muzuServer.server).get('/api/hello-async-logger');
expect(res.status).toBe(200);
expect(res.body).toEqual(
expect.objectContaining({
message: 'Hello Async Logger',
})
);
});
});

muzuServer.stop(() => {
console.log('Server stopped');
});

0 comments on commit 984e555

Please sign in to comment.