Skip to content

Media Upload

Strapi handles media files (images, documents, videos) through its Upload plugin. strapi2front generates types for media fields and provides patterns for file uploads.

strapi2front generates the StrapiMedia type:

export interface StrapiMedia {
id: number;
documentId: string;
name: string;
alternativeText: string | null;
caption: string | null;
width: number | null;
height: number | null;
formats: StrapiMediaFormats | null;
hash: string;
ext: string;
mime: string;
size: number;
url: string;
previewUrl: string | null;
provider: string;
createdAt: string;
updatedAt: string;
}
export interface StrapiMediaFormats {
thumbnail?: StrapiMediaFormat;
small?: StrapiMediaFormat;
medium?: StrapiMediaFormat;
large?: StrapiMediaFormat;
}
export interface StrapiMediaFormat {
name: string;
hash: string;
ext: string;
mime: string;
width: number;
height: number;
size: number;
url: string;
}

Media fields are populated like relations:

const article = await articleService.findOne("abc123", {
populate: ["cover", "gallery"],
});
// Single media
console.log(article.cover?.url);
// Multiple media
article.gallery?.forEach(img => {
console.log(img.url, img.width, img.height);
});

Use the formats property for responsive images:

function ResponsiveImage({ media }: { media: StrapiMedia }) {
const baseUrl = process.env.STRAPI_URL;
return (
<picture>
{media.formats?.large && (
<source
srcSet={`${baseUrl}${media.formats.large.url}`}
media="(min-width: 1024px)"
/>
)}
{media.formats?.medium && (
<source
srcSet={`${baseUrl}${media.formats.medium.url}`}
media="(min-width: 768px)"
/>
)}
{media.formats?.small && (
<source
srcSet={`${baseUrl}${media.formats.small.url}`}
media="(min-width: 640px)"
/>
)}
<img
src={`${baseUrl}${media.url}`}
alt={media.alternativeText || ""}
width={media.width || undefined}
height={media.height || undefined}
/>
</picture>
);
}

strapi2front provides two approaches for uploading files to Strapi. Enable the upload feature to generate helpers:

features: {
upload: true,
}

Server-side upload using Astro Actions. The private STRAPI_TOKEN stays on the server.

First, register the actions in src/actions/index.ts:

import { uploadAction, uploadMultipleAction } from '../strapi/shared/upload-action';
export const server = {
upload: uploadAction,
uploadMultiple: uploadMultipleAction,
};

Then use in your components:

import { actions } from 'astro:actions';
// Single file upload
const handleUpload = async (file: File) => {
const result = await actions.upload({
file,
alternativeText: 'My image description',
});
console.log('Uploaded:', result.data);
};
// Multiple files
const handleMultipleUpload = async (files: File[]) => {
const result = await actions.uploadMultiple({
files,
alternativeText: 'Gallery images',
});
console.log('Uploaded:', result.data);
};

Direct browser-to-Strapi uploads using a restricted public token.

Setup:

  1. Create a restricted token in Strapi Admin > Settings > API Tokens
  2. Set permissions: Upload > upload only (no delete, no update)
  3. Add to .env:
Terminal window
PUBLIC_STRAPI_URL=http://localhost:1337
PUBLIC_STRAPI_UPLOAD_TOKEN=your-restricted-upload-token

Usage:

import { uploadFile, uploadFiles } from '@/strapi/shared/upload-client';
// Single file
const media = await uploadFile(file, {
alternativeText: 'Image description',
});
// Multiple files
const mediaArray = await uploadFiles(files, {
caption: 'Gallery images',
});
AspectAstro ActionPublic Client
Token exposureNever (server-side)Visible in browser
Token permissionsFull accessUpload only
Delete/update filesYesNo
Works without backendNoYes

If you need custom upload logic:

async function uploadFile(file: File): Promise<number> {
const formData = new FormData();
formData.append("files", file);
const response = await fetch(`${STRAPI_URL}/api/upload`, {
method: "POST",
headers: {
Authorization: `Bearer ${STRAPI_TOKEN}`,
},
body: formData,
});
if (!response.ok) {
throw new Error("Upload failed");
}
const [uploadedFile] = await response.json();
return uploadedFile.id;
}

Media fields use numeric IDs (not documentIds). After uploading, use the returned id to associate media with content:

import { uploadFile } from '@/strapi/shared/upload-client';
import { articleService } from '@/strapi/collections/article';
// Upload and get the media object
const media = await uploadFile(coverImage);
// Create content with the file ID
await articleService.create({
title: "Article with Cover",
content: "...",
cover: media.id, // Numeric ID from upload response
});
import { uploadFile } from '@/strapi/shared/upload-client';
// Upload new file
const newMedia = await uploadFile(newCover);
// Update the content
await articleService.update("abc123", {
cover: newMedia.id,
});
// Remove media (set to null)
await articleService.update("abc123", {
cover: null,
});

Generated Zod schemas validate media fields as numbers:

export const articleCreateSchema = z.object({
title: z.string(),
// Single media (nullable)
cover: z.number().int().positive().nullable(),
// Multiple media (optional array)
gallery: z.array(z.number().int().positive()).optional(),
});

Example form with file upload using React Hook Form:

import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { articleCreateSchema, articleService } from "@/strapi/collections/article";
import { uploadFile } from "@/strapi/shared/upload-client";
function CreateArticleForm() {
const [uploading, setUploading] = useState(false);
const form = useForm({
resolver: zodResolver(articleCreateSchema),
});
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
try {
const media = await uploadFile(file, {
alternativeText: file.name,
});
form.setValue("cover", media.id);
} finally {
setUploading(false);
}
};
const onSubmit = async (data) => {
await articleService.create(data);
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<input {...form.register("title")} />
<input
type="file"
accept="image/*"
onChange={handleFileChange}
disabled={uploading}
/>
{uploading && <span>Uploading...</span>}
{/* Hidden field for file ID */}
<input type="hidden" {...form.register("cover")} />
<button type="submit" disabled={uploading}>
Create Article
</button>
</form>
);
}
function getMediaUrl(media: StrapiMedia | null): string | null {
if (!media) return null;
// If using external provider (S3, Cloudinary), URL is already absolute
if (media.url.startsWith("http")) {
return media.url;
}
// Otherwise, prepend Strapi URL
return `${process.env.STRAPI_URL}${media.url}`;
}

Strapi supports various upload providers:

  • Local - Files stored on server filesystem
  • AWS S3 - Amazon S3 storage
  • Cloudinary - Cloud-based image management
  • Uploadcare - File uploading and delivery

The StrapiMedia type works with all providers. The url field will be absolute for cloud providers.