Skip to main content

File Uploads and Media

Strapi's Upload plugin handles file storage, image processing, and media management. By default, files are stored locally, but production deployments should use cloud providers like S3 or Cloudinary.

Upload flow


Basic file upload

REST API

const formData = new FormData();
formData.append('files', fileInput.files[0]);

const response = await fetch('/api/upload', {
method: 'POST',
headers: {
Authorization: `Bearer ${jwt}`,
},
body: formData,
});

const [uploadedFile] = await response.json();
console.log(uploadedFile.url);

Upload with relation to a content entry

const formData = new FormData();
formData.append('files', fileInput.files[0]);
formData.append('ref', 'api::article.article'); // content type UID
formData.append('refId', 'documentId123'); // document ID
formData.append('field', 'cover'); // field name

const response = await fetch('/api/upload', {
method: 'POST',
headers: { Authorization: `Bearer ${jwt}` },
body: formData,
});

Multiple file upload

const formData = new FormData();
for (const file of fileInput.files) {
formData.append('files', file);
}

const response = await fetch('/api/upload', {
method: 'POST',
headers: { Authorization: `Bearer ${jwt}` },
body: formData,
});

const uploadedFiles = await response.json();

Upload providers

Local (default, development only)

Files are stored in ./public/uploads/. This is the default and requires no configuration.

AWS S3

npm install @strapi/provider-upload-aws-s3
// config/plugins.js
module.exports = ({ env }) => ({
upload: {
config: {
provider: 'aws-s3',
providerOptions: {
s3Options: {
credentials: {
accessKeyId: env('AWS_ACCESS_KEY_ID'),
secretAccessKey: env('AWS_ACCESS_SECRET'),
},
region: env('AWS_REGION', 'eu-central-1'),
params: {
Bucket: env('AWS_BUCKET'),
ACL: env('AWS_ACL', 'public-read'),
},
},
},
actionOptions: {
upload: {},
uploadStream: {},
delete: {},
},
},
},
});

Cloudinary

npm install @strapi/provider-upload-cloudinary
// config/plugins.js
module.exports = ({ env }) => ({
upload: {
config: {
provider: 'cloudinary',
providerOptions: {
cloud_name: env('CLOUDINARY_NAME'),
api_key: env('CLOUDINARY_KEY'),
api_secret: env('CLOUDINARY_SECRET'),
},
actionOptions: {
upload: {
folder: env('CLOUDINARY_FOLDER', 'strapi'),
},
uploadStream: {
folder: env('CLOUDINARY_FOLDER', 'strapi'),
},
delete: {},
},
},
},
});

S3-compatible (MinIO, DigitalOcean Spaces, Backblaze B2)

module.exports = ({ env }) => ({
upload: {
config: {
provider: 'aws-s3',
providerOptions: {
s3Options: {
credentials: {
accessKeyId: env('S3_ACCESS_KEY'),
secretAccessKey: env('S3_SECRET_KEY'),
},
endpoint: env('S3_ENDPOINT'), // e.g., https://minio.example.com
region: env('S3_REGION', 'us-east-1'),
params: {
Bucket: env('S3_BUCKET'),
},
forcePathStyle: true, // Required for MinIO
},
},
},
},
});

Image optimization and responsive formats

Strapi automatically generates responsive image formats using Sharp:

FormatMax widthDefault
thumbnail245pxEnabled
small500pxEnabled
medium750pxEnabled
large1000pxEnabled

Customizing breakpoints

// config/plugins.js
module.exports = () => ({
upload: {
config: {
breakpoints: {
xlarge: 1920,
large: 1200,
medium: 768,
small: 480,
thumbnail: 200,
},
// Limit file size (in bytes)
sizeLimit: 10 * 1024 * 1024, // 10 MB
},
},
});

Using responsive images in the frontend

function ResponsiveImage({ image }) {
const { url, formats, alternativeText, width, height } = image;

return (
<picture>
{formats?.large && (
<source media="(min-width: 1000px)" srcSet={formats.large.url} />
)}
{formats?.medium && (
<source media="(min-width: 750px)" srcSet={formats.medium.url} />
)}
{formats?.small && (
<source media="(min-width: 500px)" srcSet={formats.small.url} />
)}
<img
src={url}
alt={alternativeText || ''}
width={width}
height={height}
loading="lazy"
/>
</picture>
);
}

Upload validation

File type restrictions

Set allowed MIME types per media field in the Content-Type Builder, or validate in a lifecycle hook:

// src/index.js
module.exports = {
register({ strapi }) {
strapi.documents.use(async (context, next) => {
if (context.uid === 'plugin::upload.file' && context.action === 'create') {
const { mime } = context.params.data || {};
const allowedMimes = [
'image/jpeg',
'image/png',
'image/webp',
'image/svg+xml',
'application/pdf',
];

if (mime && !allowedMimes.includes(mime)) {
throw new Error(`File type ${mime} is not allowed`);
}
}

return next();
});
},
};

File size limits

// config/middlewares.js
module.exports = [
// ...
{
name: 'strapi::body',
config: {
formLimit: '50mb', // Form data limit
jsonLimit: '50mb', // JSON body limit
textLimit: '50mb', // Text body limit
formidable: {
maxFileSize: 20 * 1024 * 1024, // 20 MB per file
},
},
},
// ...
];

Media library API

List all files

GET /api/upload/files
GET /api/upload/files?filters[mime][$contains]=image

Get a single file

GET /api/upload/files/:id

Delete a file

DELETE /api/upload/files/:id

Update file info (alt text, caption)

const formData = new FormData();
formData.append('fileInfo', JSON.stringify({
alternativeText: 'A sunset over the mountains',
caption: 'Photo taken in the Alps',
name: 'alps-sunset',
}));

await fetch(`/api/upload?id=${fileId}`, {
method: 'POST',
headers: { Authorization: `Bearer ${jwt}` },
body: formData,
});

Folder management

Strapi 5 supports media folders for organization:

# List folders
GET /api/upload/folders

# Create a folder
POST /api/upload/folders
{ "name": "Blog Images", "parent": null }

# Upload to a specific folder
const formData = new FormData();
formData.append('files', file);
formData.append('path', '/Blog Images'); # or folder ID

Security considerations for media

  • Validate uploads server-side: never trust client-side MIME type alone
  • Scan for malware: consider integrating ClamAV for user-uploaded content
  • Use a CDN: serve media through a CDN for performance and DDoS protection
  • Set proper CORS: restrict which domains can embed your media
  • Generate signed URLs: for private media, use pre-signed S3 URLs with expiration

Common pitfalls

PitfallProblemFix
Local uploads in productionFiles lost on redeploy, no CDNUse S3 or Cloudinary
Missing PUBLIC_URLMedia URLs point to localhostSet the correct public URL
No forcePathStyle for MinIOS3 client tries virtual-hosted-style URLsSet forcePathStyle: true
Huge image uploadsMemory spikes, slow responsesSet sizeLimit and compress client-side first
No alt textAccessibility and SEO sufferRequire alternativeText in your frontend upload form

See also