Use Cases (Actualizado: 2/6/2026)

Procesamiento de imágenes con Claude Code: Sharp, Canvas, WebP/AVIF y validación

Implementa imágenes seguras con Claude Code: Sharp, Canvas, EXIF, WebP/AVIF, validación, jobs y pruebas.

Procesamiento de imágenes con Claude Code: Sharp, Canvas, WebP/AVIF y validación

El procesamiento de imágenes parece una tarea pequeña: el usuario sube una foto, la aplicación genera miniaturas y la página carga más rápido. En producción, sin embargo, se convierte en un límite técnico importante. Hay que validar el archivo, evitar nombres inseguros, eliminar EXIF, redimensionar, comprimir, elegir WebP o AVIF, mover trabajo pesado a segundo plano, proteger la privacidad y probar todo.

Claude Code puede ayudar porque esta función toca varios archivos: política de upload, endpoint, módulo de optimización, resize en navegador, worker o job, y tests. El riesgo es pedirlo de forma vaga. Si el prompt solo dice “optimiza imágenes”, el resultado puede confiar en file.type, publicar el nombre original del archivo, conservar metadatos, convertir todo a AVIF dentro del request y no probar fotos rotadas de móvil.

Para revisar el código usa fuentes primarias: documentación de Claude Code, Sharp resize API, Sharp output API, MDN File API, MDN Canvas toBlob y OWASP File Upload Cheat Sheet. Si el procesamiento en navegador se vuelve pesado, enlázalo con la guía interna de Web Workers con Claude Code.

Decide dónde se procesa

No empieces por el formato. Empieza por el límite de responsabilidad.

LugarEncaja bienConviene evitar
NavegadorPreview, resize ligero, reducir subidaValidación confiable, AVIF masivo, decisiones de privacidad
Servidor síncronoMIME, magic bytes, dimensiones, EXIF, miniatura pequeñaMuchos tamaños, AVIF lento
Job en segundo planoCatálogo de productos, regeneración CMS, migraciónRespuesta inmediata de upload

El patrón práctico es simple: el navegador mejora la experiencia, el servidor valida de nuevo y los variants pesados pasan a una cola. accept="image/*" y file.type ayudan a la interfaz, pero no son una frontera de seguridad.

flowchart LR
  Browser["Browser preview / optional resize"]
  Upload["Upload endpoint"]
  Validate["Magic bytes, size, dimensions"]
  Store["Private raw storage"]
  Job["Background variants"]
  Public["Public WebP/JPEG/AVIF"]
  Browser --> Upload
  Upload --> Validate
  Validate --> Store
  Store --> Job
  Job --> Public

Cuando pidas el cambio a Claude Code, escribe los límites explícitamente: validar MIME y magic bytes en servidor, no usar el nombre original en URL pública, aplicar rotate(), no llamar .withMetadata() salvo necesidad real, y hacer AVIF opcional. Esa lista reduce muchos errores plausibles.

Casos de uso de producto

El primer caso es ecommerce o marketplace. Los vendedores suben fotos grandes desde el móvil. El producto necesita miniatura cuadrada, imagen de tarjeta, imagen de detalle y a veces imagen social. Si comprimes demasiado, el comprador no puede ver textura, color, etiqueta o daños. WebP suele ser el inicio más estable; AVIF debe añadirse después de medir tiempo de codificación y ahorro real.

El segundo caso es avatar o foto de equipo. Lo importante es un recorte cuadrado fiable, una URL segura y privacidad. Un nombre como cliente-contrato-final.png no debe quedar en la ruta pública. Incluso si el navegador reduce la imagen, el servidor debe generar una salida sin EXIF.

El tercer caso son capturas para blog, documentación o cursos. La prioridad es que el texto se lea. Una captura que ahorra 80 KB pero vuelve borroso el nombre de un botón no sirve. Cuando imágenes y documentos se mezclan en el flujo editorial, también ayuda leer generación de PDF con Claude Code.

El cuarto caso son adjuntos privados de SaaS: facturas, documentos de verificación, capturas de soporte o archivos de administración. No deben vivir en public/uploads. Necesitan almacenamiento privado, autorización, política de borrado y auditoría.

Instalación

Los ejemplos asumen Node.js 20 o superior. Puedes adaptarlos a Next.js, Express, Hono, Astro API routes o un worker de cola.

npm i sharp file-type p-limit
npm i -D tsx typescript @types/node
mkdir -p src public/uploads

Agrega un comando específico para que CI ejecute las pruebas de imágenes.

{
  "scripts": {
    "test:images": "node --import tsx --test src/**/*.test.ts"
  }
}

Validación de upload y nombres seguros

Este módulo es la frontera de confianza. No mira la extensión; inspecciona magic bytes, lee dimensiones con Sharp, rechaza archivos demasiado grandes y bloquea imágenes animadas o multipágina para este flujo.

// src/image-policy.ts
import { randomUUID } from "node:crypto";
import { fileTypeFromBuffer } from "file-type";
import sharp from "sharp";

const MAX_BYTES = 6 * 1024 * 1024;
const MAX_PIXELS = 24_000_000;

const EXTENSION_BY_MIME = {
  "image/jpeg": ".jpg",
  "image/png": ".png",
  "image/webp": ".webp",
  "image/avif": ".avif",
} as const;

export type MimeType = keyof typeof EXTENSION_BY_MIME;

export type ImageUploadInfo = {
  mime: MimeType;
  extension: string;
  width: number;
  height: number;
  bytes: number;
  originalName: string;
};

function isAllowedMime(mime: string): mime is MimeType {
  return mime in EXTENSION_BY_MIME;
}

export async function assertImageUpload(
  buffer: Buffer,
  originalName = "upload",
): Promise<ImageUploadInfo> {
  if (buffer.byteLength === 0) {
    throw new Error("Empty file");
  }

  if (buffer.byteLength > MAX_BYTES) {
    throw new Error("Image must be 6 MB or smaller");
  }

  const detected = await fileTypeFromBuffer(buffer);

  if (!detected || !isAllowedMime(detected.mime)) {
    throw new Error("Unsupported image type");
  }

  const metadata = await sharp(buffer, { failOn: "error" }).metadata();

  if (!metadata.width || !metadata.height) {
    throw new Error("Image dimensions could not be read");
  }

  if (metadata.pages && metadata.pages > 1) {
    throw new Error("Animated images are not allowed here");
  }

  const pixels = metadata.width * metadata.height;

  if (pixels > MAX_PIXELS) {
    throw new Error("Image dimensions are too large");
  }

  return {
    mime: detected.mime,
    extension: EXTENSION_BY_MIME[detected.mime],
    width: metadata.width,
    height: metadata.height,
    bytes: buffer.byteLength,
    originalName,
  };
}

export function safeImageName(mime: MimeType): string {
  return `${randomUUID()}${EXTENSION_BY_MIME[mime]}`;
}

El nombre original puede guardarse como dato privado si la interfaz lo necesita. No debe formar parte de la URL pública. Los nombres aleatorios también evitan colisiones y problemas de Unicode.

Sharp para resize, compresión y EXIF

Sharp es una opción realista para procesar imágenes en servidor con Node.js. El detalle importante es aplicar la orientación con rotate() y no conservar metadatos salvo que haya una razón explícita. Para imágenes públicas, eliminar metadatos suele ser el valor seguro.

// src/optimize-image.ts
import { mkdir } from "node:fs/promises";
import path from "node:path";
import sharp from "sharp";

type Variant = {
  kind: "thumb" | "card" | "hero";
  width: number;
  height?: number;
};

const VARIANTS: Variant[] = [
  { kind: "thumb", width: 320, height: 320 },
  { kind: "card", width: 640 },
  { kind: "hero", width: 1280 },
];

export type OptimizedImage = {
  src: string;
  width: number;
  height: number;
  bytes: number;
  format: "webp" | "avif";
};

export async function optimizeImage(
  buffer: Buffer,
  outputDir: string,
  baseName: string,
  makeAvif = false,
): Promise<OptimizedImage[]> {
  await mkdir(outputDir, { recursive: true });

  const results: OptimizedImage[] = [];

  for (const variant of VARIANTS) {
    const resized = sharp(buffer)
      .rotate()
      .resize({
        width: variant.width,
        height: variant.height,
        fit: variant.height ? "cover" : "inside",
        withoutEnlargement: true,
      });

    const webpName = `${baseName}-${variant.kind}.webp`;
    const webpInfo = await resized
      .clone()
      .webp({ quality: 78, effort: 4 })
      .toFile(path.join(outputDir, webpName));

    results.push({
      src: `/uploads/${webpName}`,
      width: webpInfo.width,
      height: webpInfo.height,
      bytes: webpInfo.size,
      format: "webp",
    });

    if (makeAvif) {
      const avifName = `${baseName}-${variant.kind}.avif`;
      const avifInfo = await resized
        .clone()
        .avif({ quality: 45, effort: 4 })
        .toFile(path.join(outputDir, avifName));

      results.push({
        src: `/uploads/${avifName}`,
        width: avifInfo.width,
        height: avifInfo.height,
        bytes: avifInfo.size,
        format: "avif",
      });
    }
  }

  return results;
}

AVIF no es una regla universal. Puede ahorrar bytes, pero codifica más lento y no siempre aporta mucho frente a WebP. Para catálogos grandes conviene medir con imágenes reales y mantener fallback.

Endpoint de upload en Next.js

Este es un endpoint mínimo de App Router. En producción, cambia public/uploads por almacenamiento privado si las imágenes no son públicas.

// app/api/images/route.ts
import path from "node:path";
import { NextResponse } from "next/server";
import { assertImageUpload, safeImageName } from "@/src/image-policy";
import { optimizeImage } from "@/src/optimize-image";

export async function POST(request: Request) {
  const form = await request.formData();
  const file = form.get("image");

  if (!(file instanceof File)) {
    return NextResponse.json(
      { error: "image field is required" },
      { status: 400 },
    );
  }

  const buffer = Buffer.from(await file.arrayBuffer());
  const upload = await assertImageUpload(buffer, file.name);
  const storedName = safeImageName(upload.mime);
  const baseName = storedName.replace(/\.[^.]+$/, "");

  const variants = await optimizeImage(
    buffer,
    path.join(process.cwd(), "public", "uploads"),
    baseName,
    false,
  );

  return NextResponse.json({
    original: {
      width: upload.width,
      height: upload.height,
      bytes: upload.bytes,
    },
    variants,
  });
}

Este ejemplo es para medios públicos. Adjuntos de soporte, facturas y documentos privados necesitan otro modelo de entrega.

Resize en navegador

Reducir la imagen en navegador mejora la experiencia y baja el tráfico. Aun así, el servidor debe validar de nuevo.

// src/resize-in-browser.ts
export async function resizeInBrowser(
  file: File,
  maxSide = 1600,
): Promise<File> {
  const bitmap = await createImageBitmap(file);
  const scale = Math.min(1, maxSide / Math.max(bitmap.width, bitmap.height));
  const width = Math.round(bitmap.width * scale);
  const height = Math.round(bitmap.height * scale);

  const canvas = document.createElement("canvas");
  canvas.width = width;
  canvas.height = height;

  const context = canvas.getContext("2d");

  if (!context) {
    throw new Error("Canvas 2D context is not available");
  }

  context.drawImage(bitmap, 0, 0, width, height);
  bitmap.close();

  const blob = await new Promise<Blob>((resolve, reject) => {
    canvas.toBlob(
      (result) => {
        if (result) resolve(result);
        else reject(new Error("Canvas export failed"));
      },
      "image/webp",
      0.82,
    );
  });

  const outputName = file.name.replace(/\.[^.]+$/, ".webp");

  return new File([blob], outputName, {
    type: blob.type || "image/webp",
    lastModified: Date.now(),
  });
}

Canvas suele eliminar muchos metadatos al reexportar, pero la privacidad no debe depender de eso. La salida AVIF desde navegador tampoco es un camino estable para todos los usuarios.

Jobs y presupuesto de rendimiento

El request síncrono debe ser corto: validar, guardar y generar la miniatura necesaria. Imágenes de detalle, OGP, AVIF y regeneraciones masivas deben ir a un job.

Como punto inicial, usa 80 KB para avatar 320x320, 120 KB para tarjeta de ancho 640 y 250 KB para hero de ancho 1280. El presupuesto se ajusta por producto, pero tener uno evita que Claude Code elija parámetros demasiado pesados.

// src/batch-optimize.ts
import { readdir, readFile } from "node:fs/promises";
import path from "node:path";
import pLimit from "p-limit";
import { assertImageUpload, safeImageName } from "./image-policy";
import { optimizeImage } from "./optimize-image";

export async function batchOptimize(inputDir: string, outputDir: string) {
  const files = await readdir(inputDir);
  const limit = pLimit(3);

  const jobs = files.map((file) =>
    limit(async () => {
      const sourcePath = path.join(inputDir, file);
      const buffer = await readFile(sourcePath);
      const upload = await assertImageUpload(buffer, file);
      const baseName = safeImageName(upload.mime).replace(/\.[^.]+$/, "");
      const variants = await optimizeImage(buffer, outputDir, baseName, true);

      return {
        file,
        variants: variants.length,
      };
    }),
  );

  return Promise.allSettled(jobs);
}

En una cola real guarda job ID, imagen origen, variant, motivo de fallo, reintentos y rutas generadas. Si no, tarde o temprano aparecerán archivos sin dueño claro.

Fallos comunes

El primer fallo es confiar en file.type. El segundo es usar el nombre original en la URL. El tercero es ignorar la orientación EXIF y publicar fotos de móvil giradas. El cuarto es copiar .withMetadata() y conservar EXIF sin querer. El quinto es generar AVIF dentro del request y provocar timeouts.

También hay fallos de negocio. Si una captura de curso no se lee, el CTA de la plantilla pierde confianza. Si la imagen de producto tarda demasiado, el botón de compra aparece antes de que el usuario entienda qué compra. Para medir carga de imagen, CTA y compra juntos, revisa analytics con Claude Code.

Pruebas

Genera imágenes dentro del test para no depender de archivos manuales.

// src/image-policy.test.ts
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import sharp from "sharp";
import { assertImageUpload, safeImageName } from "./image-policy";
import { optimizeImage } from "./optimize-image";

test("validates and optimizes a generated image", async () => {
  const input = await sharp({
    create: {
      width: 1200,
      height: 800,
      channels: 3,
      background: "#38bdf8",
    },
  })
    .jpeg()
    .toBuffer();

  const info = await assertImageUpload(input, "masa-profile.jpg");
  assert.equal(info.mime, "image/jpeg");
  assert.equal(info.width, 1200);

  const safeName = safeImageName(info.mime);
  assert.match(safeName, /^[a-f0-9-]+\.jpg$/);

  const outDir = await mkdtemp(path.join(tmpdir(), "images-"));
  const baseName = safeName.replace(/\.[^.]+$/, "");
  const variants = await optimizeImage(input, outDir, baseName, false);

  assert.equal(variants.length, 3);
  assert.ok(variants.every((item) => item.bytes > 0));

  const thumb = await sharp(
    path.join(outDir, `${baseName}-thumb.webp`),
  ).metadata();

  assert.equal(thumb.width, 320);
  assert.equal(thumb.height, 320);
  assert.equal(thumb.exif, undefined);
});

La revisión manual debe cubrir móvil, red lenta, archivo roto, archivo enorme, foto vertical de móvil, PNG transparente y capturas con texto pequeño. Pide a Claude Code una segunda revisión centrada solo en validación, nombres, metadatos, CPU, fallback y tests.

CTA y nota de verificación

El procesamiento de imágenes protege ingresos cuando ayuda al usuario a confiar en lo que ve. Imágenes de producto rápidas, capturas legibles, avatares sin filtraciones y OGP estable apoyan el siguiente clic. Empieza con la chuleta gratuita de Claude Code; para prompts y plantillas reutilizables usa productos de ClaudeCodeLab; y para llevar reglas de upload y revisión a un repositorio real, mira training / consultation.

El 2 de junio de 2026, Masa probó este flujo en un proyecto pequeño de Next.js. El mejor resultado apareció al decir desde el principio: resize en navegador es opcional, validación en servidor es obligatoria, AVIF es opcional y el nombre original no se publica. El prompt vago “crea upload de imágenes” produjo código más débil: validación con file.type, nombres públicos originales, orientación ausente y AVIF síncrono. Dar presupuestos y fallos antes de implementar elevó mucho la calidad.

#Claude Code #procesamiento de imágenes #Sharp #WebP #validación de uploads
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.