Skip to main content

@ditsmod/body-parser

In this module, integration is done with @ts-stack/body-parser and @ts-stack/multer. By default, the following data formats are supported:

  1. application/json
  2. application/x-www-form-urlencoded
  3. text/plain
  4. application/octet-stream
  5. multipart/form-data

The first four formats in this list are handled by the @ts-stack/body-parser package, while the last one is managed by @ts-stack/multer, which is used for file uploads. Since the configuration for file uploads can vary significantly from route to route, Ditsmod provides a service to simplify file handling instead of ready-made value.

For parsing the first four formats, this module adds an interceptor to all routes that have HTTP methods specified in bodyParserConfig.acceptMethods, which by default are:

  • POST
  • PUT
  • PATCH

A ready-made example of using @ditsmod/body-parser can be viewed in the Ditsmod repository.

Installation

npm i @ditsmod/body-parser

Importing

To enable @ditsmod/body-parser globally, you need to import and export BodyParserModule in the root module:

import { rootModule } from '@ditsmod/core';
import { BodyParserModule } from '@ditsmod/body-parser';

@rootModule({
imports: [
BodyParserModule,
// ...
],
exports: [BodyParserModule]
})
export class AppModule {}

In this case, the default settings will work. If you need to change some options, you can do it as follows:

import { rootModule } from '@ditsmod/core';
import { BodyParserModule } from '@ditsmod/body-parser';

const moduleWithBodyParserConfig = BodyParserModule.withParams({
acceptMethods: ['POST'],
jsonOptions: { limit: '500kb', strict: false },
urlencodedOptions: { extended: true },
});

@rootModule({
imports: [
moduleWithBodyParserConfig,
// ...
],
exports: [moduleWithBodyParserConfig],
})
export class AppModule {}

Another option for passing the configuration:

import { rootModule, Providers } from '@ditsmod/core';
import { BodyParserModule, BodyParserConfig } from '@ditsmod/body-parser';

@rootModule({
imports: [
BodyParserModule,
// ...
],
providersPerApp: new Providers()
.useValue<BodyParserConfig>(BodyParserConfig, { acceptMethods: ['POST'] }),
exports: [BodyParserModule]
})
export class AppModule {}

Retrieving the request body

Depending on whether the controller works in context-scoped or injector-scoped mode, the result of the interceptor can be obtained in two ways:

  1. If the controller works in injector-scoped mode, the result can be obtained using the HTTP_BODY token:
import { controller, Res, inject } from '@ditsmod/core';
import { route } from '@ditsmod/routing';
import { HTTP_BODY } from '@ditsmod/body-parser';

interface Body {
one: number;
}

@controller()
export class SomeController {
@route('POST')
ok(@inject(HTTP_BODY) body: Body, res: Res) {
res.sendJson(body);
}
}
  1. If the controller is in context-scoped mode, the result can be obtained from the context:
import { controller, SingletonRequestContext } from '@ditsmod/core';
import { route } from '@ditsmod/routing';

@controller({ scope: 'ctx' })
export class SomeController {
@route('POST')
ok(ctx: SingletonRequestContext) {
ctx.sendJson(ctx.body);
}
}

Disabling the Request Body Parser

Of course, the first thing you can do to disable the request body parser is to avoid importing @ditsmod/body-parser into your module, either globally or locally. Additionally, you can disable the parser for a specific controller as follows:

import { controller } from '@ditsmod/core';
import { BodyParserConfig } from '@ditsmod/body-parser';

@controller({
providersPerRou: [
{ token: BodyParserConfig, useValue: { acceptMethods: [] } }
],
})
export class SomeController {
// ...
}

That is, this way you pass an empty array, instead of the default array ['POST', 'PUT', 'PATCH'].

File Uploads

Depending on whether the controller works in injector-scope or context-scope mode, the method of obtaining the parser and the signatures of its methods differ slightly:

  1. If the controller is running in injector-scope mode, MulterParser must be requested via DI, after which you can use its methods:
import { createWriteStream } from 'node:fs';
import { controller, Res } from '@ditsmod/core';
import { route } from '@ditsmod/routing';
import { MulterParsedForm, MulterParser } from '@ditsmod/body-parser';

@controller()
export class SomeController {
@route('POST', 'file-upload')
async downloadFile(res: Res, parse: MulterParser) {
const parsedForm = await parse.array('fieldName', 5);
await this.saveFiles(parsedForm);
// ...
res.send('ok');
}

protected saveFiles(parsedForm: MulterParsedForm) {
const promises: Promise<void>[] = [];
parsedForm.files.forEach((file) => {
const promise = new Promise<void>((resolve, reject) => {
const path = `uploaded-files/${file.originalName}`;
const writableStream = createWriteStream(path).on('error', reject).on('finish', resolve);
file.stream.pipe(writableStream);
});
promises.push(promise);
});

return Promise.all(promises);
}
}
  1. If the controller works in context-scoped mode, MulterSingletonParser must be requested via DI, after which you can use its methods:
import { createWriteStream } from 'node:fs';
import { controller, SingletonRequestContext } from '@ditsmod/core';
import { route } from '@ditsmod/routing';
import { MulterParsedForm, MulterSingletonParser } from '@ditsmod/body-parser';

@controller({ scope: 'ctx' })
export class SomeController {
constructor(protected parse: MulterSingletonParser) {}

@route('POST', 'file-upload')
async downloadFile(ctx: SingletonRequestContext) {
const parsedForm = await this.parse.array(ctx, 'fieldName', 5);
await this.saveFiles(parsedForm);
// ...
ctx.rawRes.end('ok');
}

protected saveFiles(parsedForm: MulterParsedForm) {
const promises: Promise<void>[] = [];
parsedForm.files.forEach((file) => {
const promise = new Promise<void>((resolve, reject) => {
const path = `uploaded-files/${file.originalName}`;
const writableStream = createWriteStream(path).on('error', reject).on('finish', resolve);
file.stream.pipe(writableStream);
});
promises.push(promise);
});

return Promise.all(promises);
}
}

The parsedForm object returned by the parser methods will have four properties:

  1. textFields will contain an object with values from the HTML form's text fields (if any);
  2. file will contain an object, which includes the file as a Readable stream that can be used to save the file.
  3. files will contain an array of objects, where each element has the type specified in the second point.
  4. groups will contain an object where each key corresponds to the name of a field in the HTML form, and the content of each property is an array of files with the type specified in the third point.

A maximum of two properties from these four can be filled in one parsing: the textFields property and one of the properties: file, files, or groups. Which property will be filled depends on the parser method used.

  • The single method accepts a single file from the specified form field; note the property names during object destructuring (other properties will be unfilled in this case):

    const { textFields, file } = await parse.single('fieldName');
    // OR
    const { textFields, file } = await parse.single(ctx, 'fieldName'); // For singleton.
  • The array method can accept multiple files from the specified form field:

    const { textFields, files } = await parse.array('fieldName', 5);
    // OR
    const { textFields, files } = await parse.array(ctx, 'fieldName', 5); // For singleton.
  • The any method returns the same type of data as the array method, but it accepts files with any form field names and does not have parameters to limit the maximum number of files (this limit is determined by the general configuration, which will be discussed later):

    const { textFields, files } = await parse.any();
    // OR
    const { textFields, files } = await parse.any(ctx); // For singleton.
  • The groups method accepts arrays of files from specified form fields:

    const { textFields, groups } = await parse.groups([
    { name: 'avatar', maxCount: 1 },
    { name: 'gallery', maxCount: 8 },
    ]);
    // OR
    const { textFields, groups } = await parse.groups(ctx, [
    { name: 'avatar', maxCount: 1 },
    { name: 'gallery', maxCount: 8 },
    ]); // For singleton.
  • The textFields method returns an object only with form fields that do not have type="file"; if there are file fields in the form, this method will throw an error:

    const textFields = await parse.textFields();
    // OR
    const textFields = await parse.textFields(ctx); // For singleton.

MulterExtendedOptions

In modules where @ditsmod/body-parser will be used for forms with data in multipart/form-data format, you can pass a provider with the token MulterExtendedOptions to DI. This class has two more options than the native MulterOptions class from @ts-stack/multer:

import { InputLogLevel, Status } from '@ditsmod/core';
import { MulterOptions } from '@ts-stack/multer';

export class MulterExtendedOptions extends MulterOptions {
errorStatus?: Status = Status.BAD_REQUEST;
errorLogLevel?: InputLogLevel = 'debug';
}

It is recommended to pass the provider with this token at the module level so that it applies to both MulterParser and MulterSingletonParser:

import { featureModule } from '@ditsmod/core';
import { BodyParserModule, MulterExtendedOptions } from '@ditsmod/body-parser';

const multerOptions: MulterExtendedOptions = { limits: { files: 20 }, errorLogLevel: 'debug' };

@featureModule({
imports: [
// ...
BodyParserModule
],
providersPerMod: [
{ token: MulterExtendedOptions, useValue: multerOptions },
],
})
export class SomeModule {}