Skip to content

Commit

Permalink
Merge pull request #132 from golevelup/feature/131_simple_raw_body_setup
Browse files Browse the repository at this point in the history
feat(webhooks): simple raw body setup
  • Loading branch information
WonderPanda committed Apr 7, 2020
2 parents 84ed250 + 6382c05 commit 26f5a14
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 4 deletions.
72 changes: 69 additions & 3 deletions packages/webhooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,74 @@
<img alt="license" src="https://img.shields.io/npm/l/@golevelup/nestjs-webhooks.svg">
</p>

## Description
## Motivation

Provides useful middlware and tools for buildng NestJS applications that provide webhooks to other systems.
Make it easier to build NestJS applications that consume webhooks from third party services

Exposes Middleware and utility functions for being able to process webhooks that require access to the request's raw body (eg Stripe or Slack)
## Features

- ✅ Simple utilities and middleware for enabling a request's raw body to be unmodified on specified routes
- ✅ Control raw body parsing so that you can copy the raw body onto a new configurable property on the Request object
- ✅ Provide a reusable foundation for building more specific webhook integrations

### Install

`npm install ---save @golevelup/nestjs-webhooks`

or

`yarn add @golevelup/nestjs-webhooks`

## Techniques

### Simple Raw Body Parsing

Many third party webhook providing services require that the raw body be available on the request in order for it to be validated. However, a NestJS app in it's default state automatically includes JSON parsing middleware which will modify the req.body property.

The most basic use case is keeping JSON parsing on all routes except for the ones you specifically want to exclude such that the request body property remains unchanged.

#### Step 1: Disable global raw body parsing

In your bootstrap function (normally in `main.ts`), disable body parsing. Don't worry! We'll bring it back to the other routes later.

```typescript
const app = await NestFactory.create(AppModule, undefined, {
bodyParser: false,
});
```

#### Step 2: Include middlewares in your AppModule

```typescript
import {
JsonBodyMiddleware,
RawBodyMiddleware,
} from '@golevelup/nestjs-webhooks';
```

In your `AppModule` (or equivalent top level module), include both of the above middleware in your `imports` array:

```typescript
@Module({
imports: [JsonBodyMiddleware, RawBodyMiddleware]
})
```

#### Step 3: Configure Middleware Routes

If your AppModule doesn't already, implement the `NestModule` interface from `@nestjs/common`. This will allow you to [apply middleware to specific routes](https://docs.nestjs.com/middleware#applying-middleware).

We provide a utility function to simplify configuration using the already imported middlewares. This will automatically configure your app to apply raw body parsing to the routes you specify and then to automatically apply JSON body parsing to all other routes with the exclusion of the raw routes.

```typescript
import { applyRawBodyOnlyTo } from '@golevelup/nestjs-webhooks';

class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
applyRawBodyOnlyTo(consumer, {
method: RequestMethod.ALL,
path: 'webhook',
});
}
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class TestAppModule implements NestModule {
}
}

describe('Webhooks Module (e2e)', () => {
describe('Webhooks Configurable Raw Body Module (e2e)', () => {
let app;
describe('configurable webhook middleware', () => {
beforeEach(async () => {
Expand Down
84 changes: 84 additions & 0 deletions packages/webhooks/src/tests/raw-body-and-json-middleware.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
Controller,
MiddlewareConsumer,
Module,
NestModule,
Post,
Request,
RequestMethod,
} from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest';
import { JsonBodyMiddleware, RawBodyMiddleware } from '../webhooks.middleware';
import { applyRawBodyOnlyTo } from '../webhooks.utilities';

const testBodyFn = jest.fn();

const expectedBody = { message: 'hello' };
const expectedRawBody = Buffer.from(JSON.stringify(expectedBody));
@Controller('raw')
class WebhookController {
@Post()
webhook(@Request() request) {
testBodyFn(request.body);
}
}

@Controller('api')
class ApiController {
@Post()
body(@Request() request) {
testBodyFn(request.body);
}
}

@Module({
controllers: [WebhookController, ApiController],
})
class TestAppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
applyRawBodyOnlyTo(consumer, {
method: RequestMethod.ALL,
path: 'raw',
});
}
}

describe('Webhooks Raw Body And JSON middleware', () => {
let app;
describe('only for certain endpoitns util', () => {
beforeEach(async () => {
testBodyFn.mockReset();
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [TestAppModule, RawBodyMiddleware, JsonBodyMiddleware],
}).compile();

app = moduleFixture.createNestApplication(undefined, {
bodyParser: false,
});
await app.init();
});

it('should make the raw body available on the correct request property', () => {
return request(app.getHttpServer())
.post('/raw')
.send(expectedBody)
.expect(201)
.then(() => {
expect(testBodyFn).toHaveBeenCalledTimes(1);
expect(testBodyFn).toHaveBeenCalledWith(expectedRawBody);
});
});

it('should use body parser json on all other routes', () => {
return request(app.getHttpServer())
.post('/api')
.send(expectedBody)
.expect(201)
.then(() => {
expect(testBodyFn).toHaveBeenCalledTimes(1);
expect(testBodyFn).toHaveBeenCalledWith(expectedBody);
});
});
});
});
12 changes: 12 additions & 0 deletions packages/webhooks/src/webhooks.utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ export const applyRawBodyWebhookMiddleware = (
.forRoutes(...jsonBodyRoutes);
};

export const applyRawBodyOnlyTo = (
consumer: MiddlewareConsumer,
...rawBodyRoutes: (string | RouteInfo)[]
) => {
consumer
.apply(RawBodyMiddleware)
.forRoutes(...rawBodyRoutes)
.apply(JsonBodyMiddleware)
.exclude(...rawBodyRoutes)
.forRoutes('*');
};

/**
* Applies raw body middleware to routes that saves the raw body on the request object based on
* the WebhooksModule configuration. Also adds JSON body parsing to supplied routes
Expand Down

0 comments on commit 26f5a14

Please sign in to comment.