How do I annotate an endpoint in NestJS for OpenAPI that takes Multipart Form Data

6.6k views Asked by At

My NestJS server has an endpoint that accepts files and also additional form data For example I pass a file and a user_id of the file creator in the form.

enter image description here

NestJS Swagger needs to be told explicitly that body contains the file and that the endpoint consumes multipart/form-data this is not documented in the NestJS docs https://docs.nestjs.com/openapi/types-and-parameters#types-and-parameters.

2

There are 2 answers

1
auerbachb On

Luckily some bugs led to discussion about how to handle this use case

looking at these two discussions https://github.com/nestjs/swagger/issues/167 https://github.com/nestjs/swagger/issues/417 I was able to put together the following

I have added annotation using a DTO: the two critical parts are:

in the DTO add

  @ApiProperty({
    type: 'file',
    properties: {
      file: {
        type: 'string',
        format: 'binary',
      },
    },
  })
  public readonly file: any;

  @IsString()
  public readonly user_id: string;

in the controller add

@ApiConsumes('multipart/form-data')

this gets me a working endpoint

and this OpenAPI Json

{
   "/users/files":{
      "post":{
         "operationId":"UsersController_addPrivateFile",
         "summary":"...",
         "parameters":[
            
         ],
         "requestBody":{
            "required":true,
            "content":{
               "multipart/form-data":{
                  "schema":{
                     "$ref":"#/components/schemas/UploadFileDto"
                  }
               }
            }
         }
      }
   }
}

...

{
   "UploadFileDto":{
      "type":"object",
      "properties":{
         "file":{
            "type":"file",
            "properties":{
               "file":{
                  "type":"string",
                  "format":"binary"
               }
            },
            "description":"...",
            "example":"'file': <any-kind-of-binary-file>"
         },
         "user_id":{
            "type":"string",
            "description":"...",
            "example":"cus_IPqRS333voIGbS"
         }
      },
      "required":[
         "file",
         "user_id"
      ]
   }
}

enter image description here

0
karianpour On

Here is what I find a cleaner Approach:

@Injectable()
class FileToBodyInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const ctx = context.switchToHttp();
    const req = ctx.getRequest();
    if(req.body && req.file?.fieldname) {
      const { fieldname } = req.file;
      if(!req.body[fieldname]) {
        req.body[fieldname] = req.file;
      }
    }

    return next
      .handle();
  }
}

const ApiFile = (options?: ApiPropertyOptions): PropertyDecorator => (
  target: Object, propertyKey: string | symbol
) => {
  ApiProperty({
    type: 'file',
    properties: {
      [propertyKey]: {
        type: 'string',
        format: 'binary',
      },
    },
  })(target, propertyKey);
};

class UserImageDTO {
  @ApiFile()
  file: Express.Multer.File;  // you can name it something else like image or photo
  
  @ApiProperty()
  user_id: string;
}


@Controller('users')
export class UsersController {

  @ApiBody({ type: UserImageDTO })
  // @ApiResponse( { type: ... } ) // some dto to annotate the response
  @Post('files')
  @ApiConsumes('multipart/form-data')
  @UseInterceptors(
    FileInterceptor('file'),  //this should match the file property name
    FileToBodyInterceptor,  // this is to inject the file into the body object
  ) 
  async addFile(@Body() userImage: UserImageDTO): Promise<void> { // if you return something to the client put it here
    console.log({userImage}); // all the fields and the file
    console.log(userImage.file); // the file is here

    // ... your logic
  }
}

FileToBodyInterceptor and ApiFile are general, I wish they where in the NestJs

You probably need to install @types/multer to have to Express.Multer.File