@ditsmod/body-parser
In this module, integration is done with @ts-stack/body-parser (which is a fork of the well-known ExpressJS package) and @ts-stack/multer (which is also a fork of the well-known ExpressJS package). By default, the following data formats are supported:
application/json
application/x-www-form-urlencoded
text/plain
application/octet-stream
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 is singleton or not, the result of the interceptor can be obtained in two ways:
- If the controller is non-singleton, the result can be obtained using the
HTTP_BODY
token:
import { controller, Res, route, inject } from '@ditsmod/core';
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);
}
}
- If the controller is singleton, the result can be obtained from the context:
import { controller, route, SingletonRequestContext } from '@ditsmod/core';
@controller({ isSingleton: true })
export class SomeController {
@route('POST')
ok(ctx: SingletonRequestContext) {
ctx.sendJson(ctx.body);
}
}
File Uploads
Depending on whether the controller is singleton or not, the method of obtaining the parser and the signatures of its methods differ slightly:
- If the controller is not a singleton, you need to request
MulterParser
through DI, after which you can use its methods:
import { createWriteStream } from 'node:fs';
import { controller, Res, route } from '@ditsmod/core';
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);
}
}
- If the controller is a singleton, you need to request
MulterSingletonParser
through DI, after which you can use its methods:
import { createWriteStream } from 'node:fs';
import { controller, route, SingletonRequestContext } from '@ditsmod/core';
import { MulterParsedForm, MulterSingletonParser } from '@ditsmod/body-parser';
@controller({ isSingleton: true })
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.nodeRes.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:
textFields
will contain an object with values from the HTML form's text fields (if any);file
will contain an object, which includes the file as aReadable
stream that can be used to save the file.files
will contain an array of objects, where each element has the type specified in the second point.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 thearray
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 havetype="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 {}