X Enterprises
fastify-ximagepipeline

POST /image-pipeline/upload

Upload a file for async processing — validates, stages to R2, creates a job queue entry, and returns a jobId immediately.

POST /image-pipeline/upload

Accepts a multipart form upload, validates the file (MIME type, size, sourceType), uploads it to the R2 staging prefix, creates a MediaQueue row with PENDING status, and returns a jobId immediately. Processing happens asynchronously in the background worker.

@fastify/multipart must be registered before this plugin.

Request

Method: POST
Path: /image-pipeline/upload
Content-Type: multipart/form-data

FieldTypeRequiredDescription
filebinaryYesImage file (JPEG, PNG, WebP, or GIF by default)
sourceTypestringYesOne of the configured source types — the five defaults (e.g. avatar, gallery) or custom types passed via the sourceTypes registration option
sourceIdstringYesIdentifier for the owning resource (e.g. user ID, post ID)

Response

202 Accepted

{
  "jobId": "clx1234abcd",
  "message": "File uploaded. Processing started.",
  "statusUrl": "/image-pipeline/status/clx1234abcd"
}

Throws

Errors are returned as status-coded JSON bodies ({ "error": "..." }) with the [xImagePipeline] prefix.

ErrorHTTPCause
[xImagePipeline] No file provided400Request has no file field
[xImagePipeline] sourceType and sourceId are required400Missing form fields
[xImagePipeline] Unknown sourceType: {type}. Allowed types: …400sourceType is not a configured sourceTypes key (defaults or custom types passed at registration)
[xImagePipeline] File type {mime} not allowed. Allowed types: …400MIME type not in allowedMimeTypes
[xImagePipeline] File too large. Maximum size: {n}MB413File exceeds maxFileSize
[xImagePipeline] Failed to upload file to storage500R2 upload error
[xImagePipeline] Failed to create processing job500Database error creating the job (staging object is cleaned up)
[xImagePipeline] Upload failed500Unexpected handler error

Examples

cURL upload

curl -X POST http://localhost:3000/image-pipeline/upload \
  -F "file=@/path/to/photo.jpg" \
  -F "sourceType=avatar" \
  -F "sourceId=user-abc123"

Fastify route that proxies a user avatar upload

fastify.post("/users/:id/avatar", async (request, reply) => {
  const data = await request.file();

  const form = new FormData();
  form.append("file", data.file, { filename: data.filename, contentType: data.mimetype });
  form.append("sourceType", "avatar");
  form.append("sourceId", request.params.id);

  const response = await fetch("http://localhost:3000/image-pipeline/upload", {
    method: "POST",
    body: form,
  });

  const { jobId, statusUrl } = await response.json();

  // Store jobId and poll statusUrl until COMPLETE
  return { jobId, statusUrl };
});

Custom source types

Custom sourceTypes passed at registration are honored by this route — uploads are validated against the effective configuration, not a hardcoded list. Providing sourceTypes replaces the five defaults entirely.

await fastify.register(xImagePipeline, {
  r2: { /* ... */ },
  db: prisma,
  sourceTypes: {
    product_photo: {
      variants: ["sm", "md", "lg"],
      formats: ["webp"],
      quality: 90,
      storeOriginal: true,
    },
  },
});
curl -F "file=@product.jpg" -F "sourceType=product_photo" -F "sourceId=sku-42" \
  http://localhost:3000/image-pipeline/upload

See Also

AI Context

package: "@xenterprises/fastify-ximagepipeline"
route: POST /image-pipeline/upload (multipart)
use-when: Upload an image file for async processing — returns jobId immediately; worker processes in background
fields: file (multipart), sourceType (string — any configured sourceTypes key, defaults or custom), sourceId (string)
returns: 202 { jobId, message, statusUrl }
Copyright © 2026