Saltar al contenido principal

Decoradores de subida de archivos

Express-Cargo enlaza los archivos de una solicitud multipart/form-data directamente en tu DTO, junto con los campos habituales. Dos decoradores cubren las dos formas que puede tomar una subida:

  • @UploadedFile() — un único archivo para un campo.
  • @UploadedFiles() — todos los archivos que comparten un nombre de campo, como un array.

Express-Cargo no analiza el cuerpo multipart por sí mismo. Un middleware analizador (como multer) debe ejecutarse antes de bindingCargo y adjuntar los archivos analizados a la solicitud. Luego Express-Cargo localiza esos archivos y los enlaza tal cual — los objetos de archivo nunca se modifican.

@UploadedFile(key?: string)​

Enlaza un único archivo subido. Si varios archivos comparten el nombre de campo, se enlaza el primero.

  • key: El nombre del campo del formulario. Por defecto, el nombre de la propiedad.
class UploadAvatarRequest {
@UploadedFile()
avatar!: Express.Multer.File
}

@UploadedFiles(key?: string)​

Enlaza todos los archivos subidos que comparten el nombre de campo como un array.

  • key: El nombre del campo del formulario. Por defecto, el nombre de la propiedad.
class UploadGalleryRequest {
@UploadedFiles('photos')
photos!: Express.Multer.File[]
}

Ejemplo de uso​

Configura un analizador multipart, ejecútalo como middleware de ruta antes de bindingCargo y luego lee los archivos enlazados desde el DTO. Los campos habituales como @Body se enlazan desde la misma solicitud de la manera habitual.

import express, { Request, Response } from 'express'
import multer from 'multer'
import { bindingCargo, getCargo, Body, UploadedFile, UploadedFiles } from 'express-cargo'

const upload = multer({ storage: multer.memoryStorage() })

// Un campo de texto y un único archivo en una solicitud
class UploadProfileRequest {
@Body('bio')
bio!: string

@UploadedFile('avatar')
avatar!: Express.Multer.File

@UploadedFiles('gallery')
gallery!: Express.Multer.File[]
}

const app = express()

// El middleware analizador se ejecuta primero y declara los campos esperados,
// luego bindingCargo asigna los archivos analizados al DTO.
app.post(
'/upload/profile',
upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery' }]),
bindingCargo(UploadProfileRequest),
(req: Request, res: Response) => {
const cargo = getCargo<UploadProfileRequest>(req)
res.json({
bio: cargo.bio,
avatar: cargo.avatar.originalname,
gallery: cargo.gallery.map(file => file.originalname),
})
},
)

El middleware analizador determina qué campos se aceptan:

Llamada de multerDecorador correspondiente
upload.single('avatar')@UploadedFile('avatar')
upload.array('photos')@UploadedFiles('photos')
upload.fields([{ name: 'avatar' }, ... ])un decorador por campo nombrado
upload.any()cualquiera de los anteriores, por campo

Archivos ausentes​

Un campo de subida sigue las mismas reglas de valor ausente que cualquier otra fuente. Cuando no llega ningún archivo para el campo:

  • Sin ninguno de los dos decoradores, el enlace falla con un error <field> is required.
  • Con @Optional(), el campo se enlaza a null y sus validadores se omiten.
  • Con @Default(value), se usa el valor de reserva.
class UploadRequest {
@UploadedFile()
@Optional()
avatar?: Express.Multer.File
}

@Transform no está permitido​

Los archivos se enlazan exactamente como los produjo el analizador, así que un @Transform de usuario en un campo de subida no tiene nada que transformar. Combinarlos lanza un error de esquema al inicio:

class UploadRequest {
// ❌ Inválido: los archivos se enlazan tal cual y no pueden transformarse
@UploadedFile()
@Transform((file: Express.Multer.File) => file)
avatar!: Express.Multer.File
}

Usar un analizador diferente​

De forma predeterminada, Express-Cargo lee los archivos de req.file y req.files de multer. Para integrar un analizador con una estructura de solicitud diferente — como formidable, el otro analizador multipart ampliamente usado — registra una vez un localizador de archivos personalizado al iniciar la aplicación con setCargoFileLocator. Se aplica a todas las rutas, de modo que el localizador adapta el analizador en general en lugar de conocer un campo específico.

Un localizador toma el Request de Express y devuelve los archivos subidos agrupados por nombre de campo del formulario — un Record<string, File[]> indexado por nombre de campo. Como formidable no es middleware, analiza la solicitud tú mismo y adjunta el resultado; el localizador luego lo entrega a Express-Cargo:

import express, { Request, Response, NextFunction } from 'express'
import formidable, { File } from 'formidable'
import { setCargoFileLocator } from 'express-cargo'

// 1. formidable no es middleware — analiza la solicitud tú mismo y adjunta el resultado.
// En v3, `files` ya está indexado por nombre de campo con un array por campo.
async function parseMultipart(req: Request, _res: Response, next: NextFunction) {
const [, files] = await formidable().parse(req)
;(req as any).files = files // Record<string, File[]>
next()
}

// 2. Registra el localizador una vez al inicio para que Express-Cargo lea esos archivos.
setCargoFileLocator((req: Request): Record<string, File[]> => (req as any).files ?? {})

Cada campo se asigna a un array, incluso uno que lleva un único archivo: @UploadedFile() toma la primera entrada del array de su campo, mientras que @UploadedFiles() toma todo el array. Solo la lógica de localización es específica del analizador; los objetos de archivo en sí se siguen enlazando tal cual.

Como los archivos se enlazan tal cual, su tipo y nombres de propiedad provienen del analizador: multer expone originalname y buffer, mientras que formidable expone originalFilename y filepath. Tipa cada campo del DTO con el tipo de archivo del analizador que uses, y lee las propiedades que ese analizador proporciona.