Subir archivo en formularios

Avanzado
Angular
Angular
Actualizado: 04/05/2026

Introducción

flowchart LR
    INPUT["input type=file"] --> READ["FileReader: previsualización"]
    INPUT --> FILE["Objeto File en TS"]
    FILE --> FD["FormData (multipart/form-data)"]
    FD --> HC["HttpClient.post()"]
    HC --> API["Backend recibe MultipartFile"]
    READ --> IMG["img src con DataURL"]
    API --> URL["photoUrl devuelto al frontend"]

En esta lección crearemos un componente avatar desde el que poder subir una imagen de perfil al backend desde angular.

Crear componente avatar

Ejecutar el comando:

ng generate component avatar-form

Variables y métodos para manejo del archivo

Se crean las variables:

  • photoFile que tendrá el archivo cargado por el usuario.
  • photoPreview que tendrá la imagen para previsualizarla por pantalla antes de subirla.

Resultado del archivo avatar-form.component.ts:

import { HttpClient } from '@angular/common/http';
import { Component, inject, OnInit } from '@angular/core';

export interface User {
  id?: number;
  username?: string;
  email?: string;
  photoUrl?: string;
}

@Component({
  selector: 'app-avatar-form',
  imports: [],
  templateUrl: './avatar-form.component.html',
  styleUrl: './avatar-form.component.css'
})
export class AvatarFormComponent implements OnInit {

  private httpClient = inject(HttpClient);

  photoFile: File | undefined;
  photoPreview: string | undefined;
  user: User | undefined;

  ngOnInit(): void {
    this.httpClient.get<User>('http://localhost:3000/user/account')
    .subscribe(user => this.user = user);
  }

  onFileChange(event: Event) {

  }

  save() {

  }
}

Métodos:

  • ngOnInit() carga el usuario autenticado para poder traer su avatar existente.
  • onFileChange() detectará la carga del archivo y lo guardará en las variables photoFile y photoPreview.
  • save() envía el archivo al backend.

Crear formulario HTML

En el archivo avatar-form.component.html se crea un formulario con bootstrap con un solo campo que permita cargar un archivo:

<div class="container">
    <div class="row">
        <div class="col-lg-8">
            <h1 class="my-5">Avatar de usuario</h1>
            <form>
                <div class="input-group mb-3">
                    <label class="input-group-text" for="photoUrl">Subir foto</label>
                    <input type="file" class="form-control" id="photoUrl" (change)="onFileChange($event)">
                </div>
                <button class="w-100 btn btn-primary btn-lg" type="button" (click)="save()">Subir avatar</button>
            </form>
        </div>
        <div class="col-lg-4">
            @if(photoPreview) {
            <h3 class="my-5">Nuevo avatar a subir</h3>
            <img class="img-fluid" [src]="photoPreview">
            }
            @if (!photoPreview && user?.photoUrl) {
            <h3 class="my-5">Avatar existente</h3>
            <img class="img-fluid" [src]="'http://localhost:3000/uploads/' + user?.photoUrl">
            }
        </div>
    </div>
</div>

En este HTML hay un solo input que permite cargar un archivo y automáticamente se invoca el método onFileChange() donde se gestionará el archivo.

Método onFileChange()

Este método guarda el archivo en una variable para poder subirlo más tarde en el método save() cuando el usuario pulse "Subir avatar"

También se encarga de leer el archivo para poder mostrarlo por pantalla.

  onFileChange(event: Event) {
    let target = event.target as HTMLInputElement;
    if (target.files !== null && target.files.length > 0) {
      this.photoFile = target.files[0]; // extraer el primer archivo

      // Opcional: Mostrar la imagen por pantalla para previsualizarla antes de subirla
      let reader = new FileReader();
      reader.onload = event => this.photoPreview = reader.result as string;
      reader.readAsDataURL(this.photoFile);
    }
  }

Este método solo lee un archivo. Si se quisiera leer más de uno entonces debe manejarse el array entero target.files.

Método save()

Este método es invocado cuando el usuario pulsa el botón de "Subir avatar".

Carga el archivo en un objeto FormData y lo envía al backend.

  save() {
    if (!this.photoFile) return;

    let formData = new FormData();
    formData.append('file', this.photoFile);
    // si se quiere es posible agregar más información al formData provenientes de un formulario reactivo
    // Por ejemplo otros datos del usuario:
    // formData.append('address', this.userForm.get('address')?.value)

    this.httpClient.post<User>('http://localhost:3000/user/avatar', formData)
    .subscribe(user => {
      this.photoFile = undefined;
      this.photoPreview = undefined;
      this.user = user;
    });

  }

Se envía el archivo al método http://localhost:3000/user/avatar del backend.

Caso B2B: gestores documentales en banca y AAPP

En entidades bancarias, la subida de documentos firmados (contratos hipotecarios, declaraciones FATCA/CRS) se modela con el mismo patrón input file → FormData → HttpClient.post. Vuestro equipo añade típicamente validación de tipo MIME en el cliente (PDF, JPG con firma electrónica embebida) y un hash SHA-256 calculado con crypto.subtle.digest antes del envío para verificar integridad. La organización gana porque cualquier alteración del fichero entre cliente y backend se detecta de inmediato y deja registro en el sistema de auditoría.

En administraciones públicas con sedes electrónicas, los formularios de tramitación aceptan adjuntos hasta 25 MB por documento (límite habitual de tramitadores autonómicos). Para que la experiencia siga siendo fluida con conexiones móviles, vuestro equipo configura HttpClient con reportProgress: true y muestra una barra de progreso usando los eventos HttpEventType.UploadProgress. Esto reduce las llamadas al CAU por subidas que parecían "colgadas" cuando en realidad simplemente eran lentas.

En retail con catálogos masivos, el equipo de marketing sube imágenes de productos. Aquí Angular 19 con signal simplifica el binding: el formulario reactivo usa FormControl<File | null> y el preview se construye con un computed() que reacciona al cambio de archivo. La organización gana mantenibilidad y elimina código de detección de cambios manual.

Versiones y APIs (2025)

Esta lección se aplica a Angular 19 (noviembre 2024) y a versiones recientes 17/18 con standalone: true por defecto. La directiva @if reemplaza al antiguo *ngIf y forma parte del control flow estable desde Angular 17. inject() reemplaza la inyección por constructor en componentes standalone. HttpClient soporta nativo FormData, Blob y File desde Angular 5; los progress events se habilitan con { reportProgress: true, observe: 'events' }.

Para validación robusta, considerad @angular/cdk/upload (drag-and-drop), ngx-uploader o @uppy/angular (3.0+, soporta resumable uploads tus3 y validación cliente).

Anti-patrones y pitfalls

Convertir el archivo a base64 antes del envío. Triplica el tamaño del payload y añade overhead de codificación. Enviad siempre el File original dentro de FormData, que usa multipart/form-data por debajo.

Confiar la validación de tipo solo al accept del input. El atributo accept="image/*" es una sugerencia, no una restricción de seguridad: un atacante puede subir cualquier archivo. Validad siempre en el backend con la cabecera MIME real y el magic number del fichero.

No limpiar el FileReader. Si el componente se destruye antes de que termine la lectura, el callback puede ejecutarse sobre un componente desmontado y provocar memory leaks. Usad takeUntilDestroyed() o cancelad explícitamente con reader.abort().

Subir grandes ficheros en una sola petición. Más allá de 100 MB, conviene usar uploads resumables (TUS protocol) o S3 multipart presigned URLs, donde el cliente sube directamente al bucket sin pasar por el backend.

Olvidar CORS y CSRF. El backend debe permitir multipart/form-data en CORS y, si usa cookies de sesión, validar tokens CSRF (el token suele ir en cabecera X-XSRF-TOKEN que Angular añade automáticamente con HttpClientXsrfModule).

Mostrar la imagen sin sanitizar. Aunque DataURL desde FileReader es seguro porque viene del propio cliente, evitad bypassSecurityTrust* con URLs externas no validadas.

Comparativa con alternativas

Frente a carga directa a S3 con presigned URLs, el patrón FormData → backend simplifica el flujo pero hace pasar todos los bytes por el servidor de aplicaciones. Para volúmenes grandes (catálogos retail, video assets), generar una URL prefirmada en el backend y subir desde el cliente directamente a S3 reduce coste de banda y latencia.

Frente a Cloudinary o imgix (SaaS de transformación de imágenes), el upload directo es más barato y mantiene los datos en vuestra infraestructura. Cloudinary aporta optimización automática (WebP, AVIF, redimensión on-the-fly) que justifica el coste para sitios públicos con tráfico alto.

Frente a Filepond (librería JS), Angular nativo con HttpClient es suficiente para casos sencillos. Filepond ofrece UX más pulida (drag, multi-archivo, validación) y se integra con Angular vía wrapper.

Documentación oficial

La referencia es la guía oficial de Angular para HttpClient (angular.dev/guide/http), la sección "Tracking and showing request progress" para uploads con barra de progreso, y la documentación MDN para FileReader, FormData y la API File. Para el lado backend, consultad la documentación de Spring Boot 3.4 (MultipartFile) o de Express 4.x (multer).

El patrón de subida de archivos es uno de los más comunes en aplicaciones B2B. Para vuestro equipo, dominarlo con Angular 19 y signal permite construir formularios robustos, accesibles y con buena UX, integrables con cualquier backend que acepte multipart/form-data.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Angular

Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Crear un input de tipo file en HTML para cargar una imagen. Leer la imagen y mostrarla por pantalla. Enviar la imagen a backend con FormData.