Introduction to Media Upload in Strapi via REST API
Media uploads and image management is a key feature in modern applications. Strapi CMS offers flexible media upload capabilities through its REST API, making it an ideal tool for developers looking to integrate dynamic media handling into their projects. Thus, you can find the right asset and edit and reuse it in your websites.
In this tutorial, we will learn how to upload images to Strapi headless CMS using the REST API with Postman and Next.js. Using these tools, you’ll learn how to handle media efficiently in Strapi, equipping you with practical, scalable skills for content management.
Let's dive in!
Prerequisites
You will need to equip yourself with the following to proceed.
- Node.js runtime. Version 18 or higher.
- A code editor, preferably VS Code.
- Postman for API requests.
Outline
- Introduction to Media Upload in Strapi via REST API
- Prerequisites
- Outline
- Github Repo
- What You Will Build: An Image Upload System in Strapi
- Key Terms and Explanation
- Strapi Media Library and the Upload Plugin
- How to Upload Images in Strapi
- Create a Strapi Application
- Create a Next.js Application
- Upload Single or Multiple Images to Strapi
- Link an Image to an Existing Entry in Strapi
- Display Linked Images and Food Entries
- Upload a file at Entry Creation
- View, Delete, and Update Images
- Upload Image from an API controller in Strapi
- Image Upload to Strapi using Postman
- Conclusion
Github Repo
The source code for this project can be found here.
Also, in the repo above, you can find the images, in the images
folder, we used for this tutorial.
What You Will Build: An Image Upload System in Strapi
To better understand this tutorial, we will build an image upload system that allows us to perform some image uploads and manipulations in Strapi.
Key Terms and Explanation
What is a REST API?
APIs are set of rules that allows different software applications to communicate with each other.
Representation State Transfer (REST) API, according to Wikipedia, is a software architectural style that was created to guide the design and development of the architecture for the World Wide Web.
In other words, with the REST API, we can make HTTP requests to servers to work on resources using the HTTP methods GET
, POST
, PUT
, and DELETE
.
What is Media Upload?
Media or file upload is very common in most technologies and providers, such as Nodejs, Laravel, Java, Cloudinary, AWS S3, etc., and is a fundamental aspect of web development. It is the transfer of files or media from a client machine to the server. This process is also a part of Strapi.
Strapi Media Library and the Upload Plugin
The Strapi Media Library allows the display of assets or files or via the media field of a Collection type. This plugin is activated by default.
On the other hand, the Upload plugin is responsible for powering the Media Library. You can use the Upload plugin API to upload files to Strapi or use the Media Library directly from the Strapi admin dashboard.
With the upload plugin, you can choose to upload files to the following:
Strapi maintains all the providers above. In the Strapi market place, you can find other providers such as Cloudimage, tools or Strapi plugins for image manipulation like Placeholder, Responsive image, Local Image Sharp, Image Color Palette, and so on. You can check out the Strapi market place here.
Strapi Media Library interface
Features of Strapi Media Library
- Extensive file format support
- Ability to search, sort, and filter files
- Preview of files
- Storage service integrations
- Automatic size optimization
- Live Editing
- Single or multiple uploads, etc.
To learn more, you can always visit the Strapi docs.
Types of Media in Strapi
Strapi supports various media uploads, including audio, files, images, and videos, as shown in the image below.
For this tutorial, we will only examine Image Uploads. However, the methods discussed here should work for any other media type.
✋ NOTE:
Strapi allows you to upload different kinds of files for uploading, as we will see shortly. However, we will use images for this tutorial.
Configuring the Strapi Upload Plugin
Strapi also allows us to configure the upload provider. This means that we can configure the file size, resolution, upload request timeout, responsive image size, and so on. With this, developers can have the freedom to customize and configure media uploads.
You can find information about configuring the Strapi Upload plugin here.
How to Upload Images in Strapi
There are different ways we can upload images in Strapi. They include:
- Upload Single/Multiple images in Strapi.
- Link an image by uploading it to an existing entry in Strapi.
- Link an uploaded image by its ID to an Existing Entry in Strapi.
- Upload upon entry creation (deprecated in Strapi 5; we will use another method).
- Upload a single file from an API controller in Strapi
We will see all these in action in the next sections.
Create a Strapi Application
Install Strapi v5
To install [Strapi 5](https://strapi.io/five, run the command below:
npx create-strapi@latest
Or via Yarn.
yarn create strapi
We will give it the name strapi-backend
. Please follow the prompts and answers below:
? What is the name of your project? strapi-backend
We can't find any auth credentials in your Strapi config.
Create a free account on Strapi Cloud and benefit from:
- ✦ Blazing-fast ✦ deployment for your projects
- ✦ Exclusive ✦ access to resources to make your project successful
- An ✦ Awesome ✦ community and full enjoyment of Strapi's ecosystem
Start your 14-day free trial now!
? Please log in or sign up. Skip
? Do you want to use the default database (sqlite) ? Yes
? Start with an example structure & data? No
? Start with Typescript? Yes
? Install dependencies with npm? Yes
? Initialize a git repository? No
Start Strapi Application
If the installation is successful, CD into the Strapi project and run the command below to start the Strapi application.
npm run develop
Create Food Collection and Fields
We will first create our Food Collection Type.
Navigate to Content-Type Builder > Create a new collection type and enter the display name as Food
.
Once you have entered the display name, click on Continue to create the following fields.
-
name
: This is the name of any food entry. It should be of type Short text. -
cover
: This will represent the image of any food. It should be of type Media (Single media). Ensure that you click the ADVANCED SETTINGS tab and select images as the only allowed type of media.
Select Image as the only Allowed Media
Click on the Save button. If successful, this is what the Food collection should look like:
Create Food Entries
The Food collection has been created, so let's add some entries. Create some entries with covers and also create some without covers, as shown below. For the latter without covers, we will add their covers by uploading them via the REST API using Next.js.
Create entries with and without covers
Enable Permission for Image Upload via API
We must first give permission to enable image upload via a REST API. To do this, navigate to Settings > Roles > Public, scroll down to the Upload plugin, and select the findOne
, find
and upload
actions. Here is what they do:
-
find
: This will allow us to fetch all images uploaded to the Strapi. This endpoint for this is/api/upload/files
-
findOne
: This will allow us to fetch a specified image using itsdocumentId
. The endpoint for this is/api/upload/files/:id
-
upload
: This will allow us to upload images to Strapi. The endpoint for this is/api/upload/
-
destroy
: This will allow us to delete an image. The endpoint for this is/api/upload/files/:id
.
!
Enable Permission for Image Upload
To confirm this works, open the link http://localhost:1337/api/upload/files
in your browser. You should see all the uploaded images and their data.
Fetch all images in Strapi backend
Enable API Permission for Food Collection
Enable API permissions to the Food collection, just like we did above. Click the Select all checkbox.
Enable API Permission for Food Collection
Bravo! We now have access to upload images, view images, delete images, and to view a specific image. We also have access to the Food collection. Now, let's move on to the frontend to start image uploading.
Create a Next.js Application
Install Next.js
Install Next.js by running the command below:
npx create-next-app@latest
Following the prompts after running the command above, we will name our project nextjs-frontend
. See other prompts and answers below:
✔ What is your project named? nextjs-frontend
✔ Would you like to use TypeScript? Yes
✔ Would you like to use ESLint? No
✔ Would you like to use Tailwind CSS? Yes
✔ Would you like your code inside a `src/` directory? Yes
✔ Would you like to use App Router? (recommended) Yes
✔ Would you like to use Turbopack for next dev? No
✔ Would you like to customize the import alias (@/* by default)? No
Start The Next.js App
After successfully installing Next.js, the next step is to start the application. Run the command below:
npm run dev
The command above will start our Next.js application on http://localhost:3000
, as shown below.
Install Additional Dependencies
We need to install react-toastify. This will help us with toasts or notifications.
npm i react-toastify
Ensure you CD into the directory of the Next.js app folder above, nextjs-frontend
, before running the command above.
Create TypeScript Types
Inside the ./src/app
folder, create a new file called Types.ts
. This file will contain global types for our application, the ImageEntry
and FoodEntry
.
// ./src/app/Type.ts
// Image entry data types
export interface ImageEntry {
id: number;
documentId: string;
name: string;
caption: string;
alternativeText: string;
url: string;
}
// food entry data types
export interface FoodEntry {
id: number;
documentId: string;
name: string;
cover: ImageEntry;
}
Create Next.js Components for our App
Before we begin creating the main functionalities of our application, let's create some starter components. Create a components
folder inside the ./src
folder. The components
folder is where the components below will be created.
-
Create the
SubmitButton
Component Start by creating a submit button component. Create aSubmitButton.tsx
inside the./src/components
folder.
// ./src/components/SubmitButton.tsx
"use client";
import { useFormStatus } from "react-dom";
export default function SubmitButton({ title }: { title: string }) {
const { pending } = useFormStatus();
return (
<button
type="submit"
aria-disabled={pending}
className="bg-black text-sm w-[100px] text-white px-3 py-1 rounded-lg"
>
{pending ? `${title}ing...` : title}
</button>
);
}
The component above renders a submit button that disables itself and shows "ing" (e.g., "Submitting...") during form submission, using the useFormStatus
hook. The "use client";
directive ensures it's client-side in Next.js.
-
Create
MultipleOrSingleUpload
Component This is where we will allow single or multiple image upload. Inside the./src/components
folder, create aMultipleOrSingleUpload.tsx
file and add the following code.
// ./src/components/MultipleOrSingleUpload.tsx
"use client";
import SubmitButton from "./SubmitButton";
export default function MultipleOrSingleUpload() {
return (
<form className="flex rounded h-screen lg:w-full">
<div className="divide-y w-full">
<div className="w-full my-5">
<p className=" text-base lg:text-lg">Upload Multiple Files</p>
<span className="text-sm text-[#71717a]">
Here, you can upload one or more files!
</span>
</div>
<div className="flex flex-col pt-10 gap-y-7">
<input
type="file"
name="files"
className="text-sm text-[#71717a] p-5 lg:p-0 border"
multiple
/>
<SubmitButton title="Upload" />
</div>
</div>
</form>
);
}
The component above renders a form for uploading single or multiple files. It has an input field with name
as files
, for selecting files and a submit button. It uses the SubmitButton
component, which we created above, for handling form submissions.
-
Create the
LinkToSpecifiEntry
Component This component will help us link an image to a specific entry by uploading it. Create aLinkToSpecificEntry.tsx
file inside the./src/components
folder.
// ./src/components/LinkToSpecificEntry.tsx
"use client";
import { useState } from "react";
enum LinkType {
UPLOAD_FILE = "file",
GALLERY = "gallery",
}
export default function LinkToSpecificEntry() {
const [linkType, setLinkType] = useState<string>(LinkType.UPLOAD_FILE);
return (
<div>
<div className="w-full my-5">
<p className="text-lg font-semibold">Link to a Food</p>
<span className="text-sm text-[#71717a]">
Link to a specific entry and add a cover (image)
</span>
</div>
<div className="flex justify-between items-center w-full border">
<button
type="button"
onClick={() => setLinkType(LinkType.UPLOAD_FILE)}
className={`${
linkType === LinkType.UPLOAD_FILE
? "bg-black text-white"
: "bg-white text-black"
} py-2 basis-1/2 px-3 transition-all duration-500`}
>
Link By Upload
</button>
<button
type="button"
onClick={() => setLinkType(LinkType.GALLERY)}
className={`${
linkType === LinkType.GALLERY
? "bg-black text-white"
: "bg-white text-black"
} py-2 basis-1/2 px-3 transition-all duration-500`}
>
Link from Gallery
</button>
</div>
</div>
);
}
The component above allows users to link to a specific food entry by uploading an image or selecting from a gallery (images already existing in Strapi admin dashboard). It uses useState
to toggle between two button options (upload or gallery), dynamically changing their appearance based on the selected link type. The appearance is the form for each button option, which we will see shortly.
-
Create the
LinkedImages
Component This is where we will show food entries along with their linked images. Create aLinkedImages.tsx
file inside thecomponents
folder and add the following code.
// ./src/components/LinkedImages.tsx
"use client";
import { useState, useEffect } from "react";
import { Food } from "../Types";
export default function LinkedImages() {
const [foods, setFoods] = useState<Food[]>([]);
const getFoods = async () => {};
useEffect(() => {
getFoods();
}, []);
return (
<div className=" w-full">
<div className="w-full my-5">
<p className="text-lg font-semibold">Entries with Linked Images</p>
<span className="text-sm text-[#71717a]">
This is where you find all entries along with their linked images.
</span>
</div>
</div>
);
}
The component above displays a section for entries with linked images and uses useState
to manage a list of foods. The getFoods
function is triggered via useEffect
when the component mounts to fetch and set food data. Inside the getFoods
function, we will invoke a service, which we will create shortly, to fetch food entries from the Strapi backend.
-
Create
UploadAtEntryCreation
Component The next component, theUploadAtEntryCreation
, will allow us to upload an image upon entry creation. Although this is no longer possible in Strapi v5, we will achieve it using two steps.
🤚 NOTE
In Strapi 5, creating an entry while uploading a file is no longer possible. The recommended steps are done in two steps, which we will see soon.
// ./src/components/UploadAtEntryCreation.tsx
"use client";
import SubmitButton from "./SubmitButton";
export default function UploadAtEntryCreation() {
return (
<form className="flex rounded h-screen lg:w-full">
<div className="w-full">
<div className="w-full my-5">
<p className=" text-base lg:text-lg font-semibold">
Upload a File at Entry Creation
</p>
<span className="text-sm text-[#71717a]">
Here, you can create an entry with an image!
</span>
</div>
<div className="flex flex-col pt-10 space-y-2">
<label htmlFor="cover">Food Name</label>
<input
type="text"
name="name"
placeholder="Name"
className="text-sm text-[#71717a] p-2 border"
/>
<span className="text-sm text-[#71717a]">
Select the image for this entry!
</span>
</div>
<div className="flex flex-col pt-10 gap-y-7">
<span className="flex flex-col space-y-2">
<label htmlFor="cover">Cover</label>
<input
type="file"
name="files"
className="text-sm text-[#71717a] p-5 lg:p-0 border"
/>
<span className="text-sm text-[#71717a]">
Here, you can upload two or more files!
</span>
</span>
<SubmitButton title="Upload" />
</div>
</div>
</form>
);
}
The UploadAtEntryCreation
component above renders a form that allows users to create an entry with an image upload during entry creation. It includes fields for the entry name and an option to upload one or more files. The SubmitButton
component is imported and handles form submission.
-
Create
Gallery
Component This is where all uploaded images in the Strapi backend will be displayed. Create aGallery.tsx
file inside thecomponents
folder and add the following code.
// ./src/components/Gallery.tsx
"use client";
import { useEffect, useState } from "react";
import { ImageI } from "@/app/Types";
export default function Gallery() {
const [images, setImages] = useState<ImageI[]>([]); // state to hold images
const [selectedImage, setSelectedImage] = useState<ImageI | null>(null); // for selected image
const [update, setUpdate] = useState<boolean> (false); // if image should be updated or not
// call service to fetch images
const handleFetchImages = async () => {};
// function to close the update modal
const closeModal = () => {
setUpdate(false);
};
// function to delete an image using the delete service
const onDeleteImage = async (imageId: number) => {};
// fetch images upon mounting
useEffect(() => {
handleFetchImages();
}, []);
return (
<div className=" w-full divide-y">
<div className="w-full my-5">
<p className="text-lg font-semibold">Welcome to Gallery</p>
<span className="text-sm text-[#71717a]">
This is where all uploaded images can be found.
</span>
</div>
</div>
);
}
The component above renders a gallery for managing uploaded images. It uses useState
to handle images, selected images, and a modal state for updates. The handleFetchImages
function is called inside useEffect
when the component mounts to fetch the images. Functions for closing the modal and deleting an image are also defined but not implemented.
-
Create the
TabsContainer
Component This is where we will toggle between tabs to display each component created above. Create aTabsContainer.tsx
file inside thecomponents
folder.
// .src/components/TabsContainer.tsx
"use client";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import MultipleOrSingleUpload from "./MultipleOrSingleUpload";
import { useState } from "react";
import LinkToSpecificEntry from "./LinkToSpecificEntry";
import LinkedImages from "./LinkedImages";
import Gallery from "./Gallery";
import UploadAtEntryCreation from "./UploadAtEntryCreation";
// tabs for image uploads and manipulations
enum Tabs {
MULTIPLE_OR_SINGLE = "multipleOrSingle",
SPECIFIC_ENTRY = "specificEntry",
LINKED_IMAGES = "linkedImages",
ENTRY_CREATION = "entryCreation",
GALLERY = "gallery",
}
export default function TabsContainer() {
const [tab, setTab] = useState<string>(Tabs.MULTIPLE_OR_SINGLE);
return (
<div>
<ToastContainer />
<div className="flex flex-col lg:flex-row w-full h-full lg:p-10 gap-x-10 ">
<div className="lg:flex lg:flex-col text-sm items-start h-screen basis-2/6 w-full pt-5 ">
<button
className={`${
tab == Tabs.MULTIPLE_OR_SINGLE
? " px-5 lg:px-4 text-red-500 lg:text-inherit bg-[#f4f4f5] "
: null
} px-4 py-2 rounded-lg text-left w-full`}
onClick={() => {
setTab(Tabs.MULTIPLE_OR_SINGLE);
}}
>
Upload Multiple Files
</button>
<button
className={`${
tab == Tabs.SPECIFIC_ENTRY
? " px-5 lg:px-4 text-red-500 lg:text-inherit bg-[#f4f4f5] "
: null
} px-4 py-2 rounded-lg text-left w-full`}
onClick={() => {
setTab(Tabs.SPECIFIC_ENTRY);
}}
>
Link to a Specific Entry
</button>
<button
className={`${
tab == Tabs.LINKED_IMAGES
? " px-5 lg:px-4 text-red-500 lg:text-inherit bg-[#f4f4f5] "
: null
} px-4 py-2 rounded-lg text-left w-full`}
onClick={() => {
setTab(Tabs.LINKED_IMAGES);
}}
>
Linked Images
</button>
<button
className={`${
tab == Tabs.ENTRY_CREATION
? " px-5 lg:px-4 text-red-500 lg:text-inherit bg-[#f4f4f5] "
: null
} px-4 py-2 rounded-lg text-left w-full`}
onClick={() => {
setTab(Tabs.ENTRY_CREATION);
}}
>
Upload at Entry creation
</button>
<button
className={`${
tab == Tabs.GALLERY
? " px-5 lg:px-4 text-red-500 lg:text-inherit bg-[#f4f4f5] "
: null
} px-4 py-2 rounded-lg text-left w-full`}
onClick={() => {
setTab(Tabs.GALLERY);
}}
>
Gallery
</button>
</div>
<div className="h-screen w-full flex flex-col gap-y-10 basis-4/6">
{tab === Tabs.MULTIPLE_OR_SINGLE ? (
<MultipleOrSingleUpload />
) : tab === Tabs.SPECIFIC_ENTRY ? (
<LinkToSpecificEntry />
) : tab === Tabs.LINKED_IMAGES ? (
<LinkedImages />
) : tab === Tabs.ENTRY_CREATION ? (
<UploadAtEntryCreation />
) : (
<Gallery />
)}
</div>
</div>
</div>
);
}
The TabsContainer
component above renders a tabbed interface for managing various upload and image-related tasks we have mentioned previously, using useState
to control which tab is active. The component includes buttons for selecting tabs, such as "Upload Multiple Files" and "Gallery," and conditionally renders the corresponding content based on the selected tab. It also integrates the ToastContainer
from react-toastify
to display toasts or notifications.
Display All Created Components
We want all the tab components created above to be visible. Here are the steps we will take to achieve this:
-
Step 1: Remove Default CSS
Ensure you visit the
./src/app/global.css
file and update it with the following code:
@tailwind base;
@tailwind components;
@tailwind utilities;
By updating the above, we have removed any custom default CSS.
-
Step 2: Render
TabsContainer
Component Update the code in the./src/page.tsx
file with the following:
// ./src/app/page.tsx
import TabsContainer from "./components/TabsContainer";
export default function Home() {
return (
<div className="min-h-screen p-3 lg:p-20">
<div className=" lg:px-14 py-5">
<p className="text-2xl lg:text-4xl font-bold mb-4">
Image Upload to Strapi
</p>
<span className="text-slate-400">
Let's demonstrate image upload to Strapi Content types using the REST
API
</span>
<TabsContainer />
</div>
</div>
);
}
This is what our app should look like
Create Server Actions for our App
The next step is to create Next.js server actions. These are asynchronous functions that are executed on the server.
With server actions, you can ensure that heavy computations and sensitive operations are performed securely on the server, reducing the load on the client.
Here are the server actions we will create:
- Multiple or single upload server action: This will allow us to upload a single or multiple images to Strapi.
- Link to a specific entry server action (link an already uploaded image and link by uploading an image)
- Upload at entry creation server action
- Update image server action
Before we proceed to do that, let's create some constants and default actions. Inside the ./src/app
folder, create a file called actions.tsx
and add the following code:
"use server";
const STRAPI_URL: string = "http://localhost:1337";
// accepted image types
const ACCEPTED_IMAGE_TYPES = [
"image/jpeg",
"image/jpg",
"image/png",
"image/webp",
];
// upload single or multiple images
export async function uploadMultipleOrSingleAction(
prevState: any,
formData: FormData
) {}
// Upload an Image and link to an entry
export async function LinkByUploadAction(
prevState: any,
formData: FormData
) {}
// Link an Uploaded image to an Entry by ID
export async function linkFromGalleryAction(
prevState: any,
formData: FormData
) {}
// Upload image at entry creation
export async function uploadAtEntryCreationAction(
prevState: any,
formData: FormData
){}
// update mage file info action
export async function updateImageAction(
prevState: any,
formData: FormData
){}
In the code above, STRAPI_URL
stores the base URL of the Strapi API, and ACCEPTED_IMAGE_TYPES
is an array that lists the accepted image formats (JPEG, JPG, PNG, and WEBP) for file uploads. The "use server"
directive ensures the code runs on the server side. We then created the server actions that our Next.js server will perform, which we will later modify.
🤚 NOTE
We will update the server actions above later in this tutorial.
Create Services for Our App
Next, we will create services for our application. Services are reusable functions that communicate with other services or external systems and can be called anytime.
For this application, we will create 3 services:
- Fetch Images Service: This will help fetch images from the Strapi backend.
- Fetch Foods Service: This will help fetch food entries from the Strapi backend.
- Delete image service: This will help delete an image from the Strapi backend.
Proceed to create a file called services.ts
and add the following code.
// ./src/app/services.ts
const STRAPI_URL: string = "http://localhost:1337";
export const fetchImages = async () => {
try {
// fetch images from Strapi backend
const response = await fetch(`${STRAPI_URL}/api/upload/files`);
// if response is not ok
if (!response.ok) {
const errorDetails = await response.text();
throw new Error(
`Error fetching images: ${response.status} ${response.statusText} - ${errorDetails}`
);
}
// return fetched images
const result = await response.json();
return result;
} catch (error: any) {
throw new Error(`Failed to fetch images: ${error.message}`);
}
};
export const fetchFoods = async () => {
try {
// fetch foods from Strapi backend
const response = await fetch(`${STRAPI_URL}/api/foods?populate=*`);
// if response is not ok
if (!response.ok) {
const errorDetails = await response.text();
throw new Error(
`Error fetching Foods: ${response.status} ${response.statusText} - ${errorDetails}`
);
}
// return fetched foods
const result = await response.json();
return result;
} catch (error: any) {
// throw new error
throw new Error(`Failed to fetch images: ${error.message}`);
}
};
export const deleteImage = async (imageId: number) => {
try {
// make a DELETE request using image id.
const response = await fetch(`${STRAPI_URL}/api/upload/files/${imageId}`, {
method: "DELETE",
});
// if response is not ok
if (!response.ok) {
const errorDetails = await response.text();
throw new Error(
`Error deleting food entry: ${response.status} ${response.statusText} - ${errorDetails}`
);
}
} catch (error: any) {
// throw new error
throw new Error(`Failed to delete entry: ${error.message}`);
}
};
Here is what the following services above do:
-
fetchImages
: Retrieves all uploaded images from the Strapi backend by sending aGET
request to the/api/upload/files
endpoint. It processes the response, throwing errors if the request is unsuccessful, and returns the list of images in JSON format. -
fetchFoods
: Retrieves all food entries from the Strapi backend, including any populated relationships, by sending aGET
request to the/api/foods
endpoint with apopulate=*
query parameter. It checks for errors, handles unsuccessful responses, and returns the data in JSON format. -
deleteImage
: Deletes a specific image from the Strapi backend by sending aDELETE
request to/upload/files/{imageId}
using the image’s unique ID. If the deletion fails, it throws a custom error message.
Now that we have created the services let's implement them.
Upload Single or Multiple Images to Strapi
Now, let's modify the MultipleOrSingleUpload
component and the uploadMultipleOrSingleAction
to implement this.
We will do this in 2 steps:
1. Update the uploadMultipleOrSingleAction
Server Action
Update the uploadMultipleOrSingleAction
server action so as to invoke it in the MultipleOrSingleImageUpload
component.
// ./src/actions.ts
...
export async function uploadMultipleOrSingleAction(
prevState: any,
formData: FormData
) {
try {
const response = await fetch(`${STRAPI_URL}/api/upload`, {
method: "post",
body: formData,
});
const result = await response.json();
if (result.error) {
return {
uploadError: result.error.message,
uploadSuccess: null,
};
}
return {
uploadError: null,
uploadSuccess: "Images uploaded successfully",
};
} catch (error: any) {
return {
uploadError: error.message,
uploadSuccess: null,
};
}
}
Here is what the code above does:
-
prevState
: This is the previous state of the form submission, which contains propertiesuploadError
anduploadSuccess
. It's provided whenuseFormState
is invoked. -
formData
: This is an instance of the FormData API containing the actual form data that the user wants to upload, which in this case are images. - The function sends a
POST
request to the Strapi endpoint/api/upload
to upload images. TheformData
, from the upload, is passed as the body of the request. If there's an error in the response (like the upload failing), it returns an error message. If the upload is successful, it returns a success message.
2. Update the MultipleOrSingleUpload
Component
Let's modify the MultipleOrSingleUpload
component form to trigger the server action above.
// .src/components/MultipleOrSingleImageUpload
"use client";
import { uploadMultipleOrSingleAction } from "@/app/actions";
import { useFormState } from "react-dom";
import { Ref, useRef } from "react";
import SubmitButton from "./SubmitButton";
import { toast } from "react-toastify";
const initialState = {
uploadError: null,
uploadSuccess: null,
};
export default function MultipleOrSingleImageUpload() {
const [state, formAction] = useFormState(
uploadMultipleOrSingleAction,
initialState
);
const formRef = useRef<HTMLFormElement | null>(null);
if (state?.uploadSuccess) {
formRef?.current?.reset();
toast.success(state?.uploadSuccess);
}
return (
<form
ref={formRef}
action={formAction}
className="flex rounded h-screen lg:w-full"
>
<div className="divide-y w-full">
<div className="w-full my-5">
<p className=" text-base lg:text-lg">Upload Multiple Files</p>
<span className="text-sm text-[#71717a]">
Here, you can upload two or more files!
</span>
</div>
<div className="flex flex-col pt-10 gap-y-7">
<input
type="file"
name="files"
className="text-sm text-[#71717a] p-5 lg:p-0 border"
multiple
/>
{state?.uploadError ? (
<span className="text-red-500">{state?.uploadError}</span>
) : null}
<SubmitButton title="Upload" />
</div>
</div>
</form>
);
}
Here is what we did in the code above:
-
state
: This object is created byuseFormState
, which tracks the state of the upload (whether there’s an error or success). - The initial state,
initialState
, includesuploadError: null
anduploadSuccess: null
. They will be used to pass error and success messages between theuploadMultipleOrSingleAction
server action and the form above. -
formAction
: This function links to theuploadMultipleOrSingleAction
server action and is triggered when the form is submitted. -
formRef
: This is a reference to the form DOM element, created usinguseRef
. It’s used to reset the form fields after a successful upload. - The
<input>
element has themultiple
attribute, which allows users to select more than one file at a time. Thename="files"
attribute is crucial because it matches the expected key when submitting files in theFormData
. - If
state.uploadError
contains a message, it will be displayed in red below the input field using a conditional rendering block. - If the
state.uploadSuccess
contains a success message (indicating that the files were uploaded successfully), the form is reset by callingformRef.current.reset()
. - Additionally, a toast notification is displayed using
toast.success(state.uploadSuccess)
to notify the user that the upload was successful.
When everything is set and done, we should be able to upload a single or multiple images to our Strapi backend. See the GIF below:
Single or Multiple Image upload
When we visit the Strapi backend, we should see the images we just uploaded.
Link an Image to an Existing Entry in Strapi
Like we pointed out in the beginning, we will do this in two ways. One is to link to an already existing image in Strapi backend by its ID to an entry and the other is to upload an image and link to a specific food entry.
Method 1: Link an Image to an Entry in Strapi
When we click on the "Link to a Specific Entry" tab, we can see "Link by Upload" and "Link from Gallery". In this method 1, we will link by upload.
-
Step 1: Update the
LinkByUploadAction
Server Action
// ./src/actions.ts
...
export async function LinkByUploadAction(prevState: any, formData: FormData) {
try {
// Convert formData into an object to extract data
const data = Object.fromEntries(formData);
// Create a new FormData object to send to the server
const formDataToSend = new FormData();
formDataToSend.append("files", data.files); // The image file
formDataToSend.append("ref", data.ref); // The reference type for Food collection
formDataToSend.append("refId", data.refId); // The ID of the food entry
formDataToSend.append("field", data.field); // The specific field to which the image is linked, i.e., "cover"
// Make the API request to Strapi to upload the file and link it to the specific entry
const response = await fetch(`${STRAPI_URL}/api/upload`, {
method: "post",
body: formDataToSend,
});
// upload respone
const result = await response.json();
// Handle potential errors from the API response
if (result.error) {
return {
uploadError: result.error.message,
uploadSuccess: null,
};
}
// Return success if the upload and linking are successful
return {
uploadSuccess: "Image linked to a food successfully!",
uploadError: null,
};
} catch (error: any) {
// Catch any errors that occur during the process
return {
uploadError: error.message,
uploadSuccess: null,
};
}
}
In the code above, prevState
holds the previous state of the upload process. formData
contains the form data submitted by the user, including the file and other necessary fields (files
, ref
, refId
, and field
). The ref
represents the reference type for Food collection, which we will add as a hidden value in the form below. The refId
represents the ID of the food entry. Then we have the field
, which represents the cover
field of the food entry that we want to link.
-
Step 2: Create a Form for Linking by Upload
We have to trigger the server action above. To do this, inside the
./src/components
folder, create a folder calledform
. Inside the newform
folder, create a file calledLinkByUpload.tsx
and add the following code:
// ./src/components/LinkByUpload.tsx
import { useEffect, useState } from "react";
import { useFormState } from "react-dom";
import { toast } from "react-toastify";
import { LinkByUploadAction } from "@/app/actions";
import SubmitButton from "@/components/SubmitButton";
import { fetchFoods } from "@/app/services";
import { FoodEntry } from "@/app/Types";
// initial state
const initialState = {
uploadError: null,
uploadSuccess: null,
};
export default function LinkByUpload() {
const [state, formAction] = useFormState(LinkByUploadAction, initialState);
const [foods, setFoods] = useState<FoodEntry[]>([]);
const handleFetchFoods = async () => {
const result = await fetchFoods();
setFoods(result?.data);
};
useEffect(() => {
handleFetchFoods();
}, []);
if (state?.uploadSuccess) {
toast.success(state?.uploadSuccess);
}
return (
<div className="w-full">
<form action={formAction} className="flex rounded w-full">
<div className="flex flex-col pt-10 gap-y-7 w-full">
<div className="flex flex-col space-y-2">
<label htmlFor="name">Food</label>
<select name="refId" className="border p-2 text-[#71717a] text-sm">
{foods.map((food) => {
return (
<option key={food.id} value={food.id}>
{" "}
{food.name}
</option>
);
})}
</select>
<span className="text-sm text-[#71717a]">
Select the food you want to add Image to
</span>
<div className="flex flex-col space-y-2 pt-10">
<label htmlFor="cover">Cover</label>
<input
type="file"
name="files"
className="text-sm text-[#71717a] border"
/>
<span className="text-sm text-[#71717a]">
Select an image to link to a food
</span>
</div>
{state?.uploadError ? (
<span className="text-red-500">{state?.uploadError}</span>
) : null}
<input type="hidden" name="ref" value="api::food.food" />
<input type="hidden" name="field" value="cover" />
<div className="pt-5">
<SubmitButton title="Link" />
</div>
</div>
</div>
</form>
</div>
);
}
From the code above, we have the foods
state that stores the list of food items fetched from the server, which will be displayed in the dropdown menu. In other words, the component fetches food items using fetchFoods
and populates the dropdown <select>
menu. With that, users can select a food item, upload an image, and submit the form. Note that we have some hidden values such as ref
. This has a value of api::food.food
, which is the reference type for the food collection. It also has the refId
as the id
value of any food selected. The form triggers the LinkByUploadAction
server action to perform the actual file upload and linking.
-
Step 3: Update the
LinkToSpecificEntry
component Now, we will have to update theLinkToSpecificEntry
component to include theLinkByUpload
form we created above. Head over to./src/components/LinkToSpecificEntry.tsx
and replace the content with the following code:
// ./src/components/LinkToSpecificEntry.tsx
"use client";
import { useState } from "react";
import LinkByUpload from "./form/LinkByUpload";
// linking type
enum LinkType {
UPLOAD = "upload",
GALLERY = "gallery",
}
export default function LinkToSpecificEntry() {
const [linkType, setLinkType] = useState<LinkType>(LinkType.UPLOAD);
return (
<div>
<div className="w-full my-5">
<p className="text-lg font-semibold">Link to a Food</p>
<span className="text-sm text-[#71717a]">
Link to a specific entry and add a cover (image)
</span>
</div>
<div className="flex justify-between items-center w-full border">
<button
type="button"
onClick={() => setLinkType(LinkType.UPLOAD)}
className={`${
linkType === LinkType.UPLOAD
? "bg-black text-white"
: "bg-white text-black"
} py-2 basis-1/2 px-3 transition-all duration-500`}
>
Link By Upload
</button>
<button
type="button"
onClick={() => setLinkType(LinkType.GALLERY)}
className={`${
linkType === LinkType.GALLERY
? "bg-black text-white"
: "bg-white text-black"
} py-2 basis-1/2 px-3 transition-all duration-500`}
>
Link from Gallery
</button>
</div>
{linkType === LinkType.UPLOAD ? <LinkByUpload /> : null }
</div>
);
}
In the code above, the LinkToSpecificEntry
component creates an interface where users can either upload a new image or (in the future, which we will see soon) choose one from a gallery to link to a specific food entry. The form interaction is handled through state changes and conditional rendering, with the actual file upload functionality delegated to the LinkByUpload
component.
Now, let's test our code:
Link an image by uploading to an entry
Head over to the Strapi backend to see if this was successful.
As shown in the image below, the linked image for "Pizza" didn't show. Now click on the Pizza entry and click on the "Published" tab. There, you will find the linked image.
Linked Image Shows only on Published Tab or verion
This means that the entry was published directly after linking. This happens as a result of the Draft and Publish feature of Strapi. But we don't want this behaviour. So, let's disable Draft and Publish in Food collection.
- Disable Draft and Publish for Food Collection Click on the Content-Type Builder and click on the Edit button for Food collection. Once the modal opens up, click on the ADVANCED SETTINGS tab and disable Draft and Publish. Ensure that you also click the Finish button and wait for the server to restart.
Once that is done, we can now see the linked image for Pizza!
We can now proceed to link an already uploaded image to an existing entry.
Method 2: Link an Already Uploaded Image to an Existing Entry in Strapi
The next method would be to link an already uploaded image in the Strapi backend. In other words, we want to link using an image ID. To do this, we have to fetch images in the Strapi backend, thanks to the fetchImages
service.
-
Step 1: Configure Next.js For Image Links
Since we will be displaying images uploaded to the Strapi backend, which is an external source, we need to protect our application from malicious users; configuration is required in order to use external images. Locate the
./next.config.mjs
file and replace the content with the following code:
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
hostname: "localhost"
}
]
}
};
export default nextConfig;
With this, we have configured localhost
to be a secure link to our images.
-
Step 2: Update
LinkFromGallery
Server Action Update theLinkFromGalleryAction
action with the following code:
// ./src/app/actions.ts
...
export async function linkFromGalleryAction(
prevState: any,
formData: FormData
) {
try {
const data = Object.fromEntries(formData);
const response = await fetch(`${STRAPI_URL}/api/foods/${data?.refId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
cover: data?.imageId,
},
}),
});
const result = await response.json();
if (result.error) {
return {
uploadError: result.error.message,
uploadSuccess: null,
};
}
return {
uploadSuccess: "Image linked to a food successfully!",
uploadError: null,
};
} catch (error: any) {
return {
uploadError: error.message,
uploadSuccess: null,
};
}
}
In the code above, we updated the LinkFromGallery
server action. It takes parameters prevState
that hold the previous state of the form. formData
contains the form data submitted by the user, including the refId
(food ID) and imageId
(selected image ID).
The Object.fromEntries(formData)
converts the form data from the FormData
format into a standard object for easier manipulation. The API request is sent to STRAPI_URL/api/foods/${data?.refId}
to update the specific food entry with the selected image.
The body of the request contains the new image's ID (imageId
) to be set and linked as the food's cover image.
-
Step 3: Create a Form to Link by Gallery
Inside the
./src/components/form
folder, create another file calledLinkByGallery.tsx
and add the following code:
// ./src/components/form/LinkByGallery.tsx
import { useEffect, useState, useRef } from "react";
import { useFormState } from "react-dom";
import Image from "next/image";
import { fetchFoods, fetchImages } from "@/app/services";
import { toast } from "react-toastify";
import SubmitButton from "@/components/SubmitButton";
import { linkFromGalleryAction } from "@/app/actions";
import { FoodEntry, ImageEntry } from "@/app/Types";
const STRAPI_URL: string = "http://localhost:1337";
const initialState = {
uploadError: null,
uploadSuccess: null,
};
export default function LinkByGallery() {
const [state, formAction] = useFormState(linkFromGalleryAction, initialState);
const [foods, setFoods] = useState<FoodEntry[]>([]);
const [images, setImages] = useState<ImageEntry[]>([]);
const [selectedImageId, setSelectedImageId] = useState<number | string>("");
const formRef = useRef<HTMLFormElement | null>(null);
const handleFetchFoods = async () => {
const result = await fetchFoods();
setFoods(result?.data);
};
const handleFetchImages = async () => {
const images = await fetchImages();
setImages(images);
};
if (state?.uploadSuccess) {
formRef?.current?.reset();
toast.success(state?.uploadSuccess);
state.uploadSuccess = "";
}
useEffect(() => {
handleFetchFoods();
handleFetchImages();
}, []);
return (
<div className="w-full">
<form ref={formRef} action={formAction} className="flex rounded w-full">
<div className="w-full">
<div className="flex flex-col pt-10 gap-y-7">
<div className="flex flex-col space-y-2">
<label htmlFor="name">Food</label>
<select
name="refId"
className="border p-2 text-[#71717a] text-sm w-full"
id="food"
>
{foods.map((food) => {
return <option value={food.documentId}>{food.name}</option>;
})}
</select>
<span className="text-sm text-[#71717a]">
Select the food you want to add Image to
</span>
</div>
</div>
{images?.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-5 w-full pt-10">
{images?.map((image) => (
<div
className={`${
selectedImageId === image?.id ? "border-2 border-black" : ""
} h-[200px] w-[400]px group relative`}
onClick={() => setSelectedImageId(image?.id)}
>
{selectedImageId === image?.id && (
<div className="flex items-center justify-center absolute top-0 left-0 w-full h-full">
<span className="absolute z-50 text-white font-extrabold">
selected
</span>
</div>
)}
<Image
src={`${STRAPI_URL}${image?.url}`}
alt={image.name}
width={400}
height={100}
className={` ${
selectedImageId === image?.id ? " opacity-50 " : " "
} transition-all duration-300 opacity-100 h-full w-full max-w-full rounded-lg group-hover:opacity-50`}
/>
</div>
))}
</div>
) : (
<p className="w-full text-orange-300 pt-5">No Images in Gallery.</p>
)}
<p className="pt-2">
{state?.uploadError ? (
<span className="text-red-500">{state?.uploadError}</span>
) : null}
</p>
<input type="hidden" name="imageId" value={selectedImageId} />
<div className="pt-5">
<SubmitButton title="Link" />
</div>
</div>
</form>
</div>
);
}
In the LinkByGallery
form component above, states foods
are created to store the list of available foods fetched from the server, and images
are created to store the list of images in the gallery. And selectedImageId
, which will store the ID of the image selected by the user from the gallery.
We fetched data using the handleFetchFoods()
function which fetches the list of available food entries from the server using fetchFoods()
service. And the handleFetchImages()
which fetches the list of images from the gallery using `fetchImages() service.
When an image is clicked, its imageId
is stored in selectedImageId
. The selected image is visually highlighted by applying a border to its container. The form is connected to useFormState(linkFromGalleryAction)
, which ties the form submission to the linkFromGalleryAction
server action.
A hidden field named imageId
is used to store the ID of the selected image, which is then sent to the server when the form is submitted.
After submission, if the operation is successful, the form is reset, and a success message is displayed using toast.success()
.
If an error occurs, the error message is displayed below the form.
- Step 4: Import
LinkByGallery
to theLinkToSpecificEntry
component.
`js
// .src/components/LinkToSpecificEntry.tsx
"use client";
import { useState } from "react";
import LinkByUpload from "./form/LinkByUpload";
import LinkByGallery from "./form/LinkByGallery";
// linking type
enum LinkType {
UPLOAD = "upload",
GALLERY = "gallery",
}
export default function LinkToSpecificEntry() {
const [linkType, setLinkType] = useState(LinkType.UPLOAD);
return (
Link to a Food
Link to a specific entry and add a cover (image)
type="button"
onClick={() => setLinkType(LinkType.UPLOAD)}
className={
${
linkType === LinkType.UPLOAD
? "bg-black text-white"
: "bg-white text-black"
} py-2 basis-1/2 px-3 transition-all duration-500
}>
Link By Upload
type="button"
onClick={() => setLinkType(LinkType.GALLERY)}
className={
${
linkType === LinkType.GALLERY
? "bg-black text-white"
: "bg-white text-black"
} py-2 basis-1/2 px-3 transition-all duration-500
}>
Link from Gallery
{linkType === LinkType.UPLOAD ? : }
);
}
`
Let's see this in action:
Now, when we check the Strapi backend, we should see that the food entry and the image you selected have been linked.
With this method, we could link an already uploaded image in Strapi backend to a food entry. And we can also update or change the linked image of an entry.
In the next section, we will see how to upload a file at entry creation.
Display Linked Images and Food Entries
On the tabs section of our application, we can see that we have the "Linked Images" section. This should show us food entries along with their linked images.
Let's do this by updating the ./src/components/LinkedImages.tsx
file.
`js
// ./src/components/LinkedImages.tsx
"use client";
import { useEffect, useState } from "react";
import Image from "next/image";
import { fetchFoods } from "@/app/services";
import { FoodEntry } from "@/app/Types";
const STRAPI_URL: string = "http://localhost:1337";
export default function LinkedImages() {
const [foods, setFoods] = useState([]); // foods state
// get foods using the featchFoods service
const getFoods = async () => {
const result = await fetchFoods();
setFoods(result?.data);
};
// fetch foods upon mounting
useEffect(() => {
getFoods();
}, []);
return (
Entries with Linked Images
This is where you find all uploaded images so as update anyone.
{foods?.length > 0 && (
{foods?.map((food) => (
{food?.name}
{food?.cover?.url ? (
src={`${STRAPI_URL}/${food?.cover?.url}`}
alt={food?.cover?.name}
width={200}
height={300}
className="transition-all duration-300 opacity-100 h-full w-full max-w-full rounded-lg group-hover:opacity-50"
/>
) : (
No linked image
)}
))}
)}
{foods?.length <= 0 &&
No foods and images linked
});
}
`
This code above defines the LinkedImages
component, which fetches and displays food entries from Strapi along with their linked images. The foods
state stores the fetched data, which is updated by the getFoods
function. The useEffect
hook triggers getFoods
when the component mounts to fetch the food data from the fetchFoods
service. The component maps over the food array and displays each food’s name and image using the Image component. If a food has no linked image, it displays a "No linked image" message. The STRAPI_URL
constant holds the Strapi server URL for image paths.
This is what the linked images and foods tab should look like:
Food Entries and Their Linked Images
As we can see, most of the food entries has been linked. Proceed to the "Link to a Specific" entry tab to link any food entry that has no cover or image linked to it.
Upload a file at Entry Creation
Since uploading a file at entry creation is no longer possible in Strapi 5, we will use the recommended programmatic two-step process. First, we have to upload the image to Strapi backend, and then create the entry with the created image ID.
Update the uploadAtEntryCreationAction
Server Action
We will have to update the uploadAtEntryCreationAction
server action with the following code:
`js
// ./src/actions.ts
...
export async function uploadAtEntryCreationAction(
prevState: any,
formData: FormData
) {
try {
const data = Object.fromEntries(formData);
// upload file
const uploadResponse = await fetch(`${STRAPI_URL}/api/upload`, {
method: "post",
body: formData,
});
// get result of upload
const uploadedImage = await uploadResponse.json();
// if error
if (uploadedImage.error) {
return {
uploadError: uploadedImage.error.message,
uploadSuccess: null,
};
}
// create entry
const newEntry = {
data: {
name: data.name,
cover: uploadedImage[0]?.id,
},
};
// Create entry API request
const response = await fetch(`${STRAPI_URL}/api/foods`, {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(newEntry),
});
const result = await response.json();
if (result.error) {
return {
uploadError: result.error.message,
uploadSuccess: null,
};
}
return {
uploadError: null,
uploadSuccess: "Images uploaded successfully",
};
} catch (error: any) {
return {
uploadError: null,
uploadSuccess: "Images uploaded successfully",
};
}
}
`
In the code above, we make a
POST
request to the Strapi API's upload endpoint (/api/upload
) to upload the file contained informData
. The result is stored inuploadedImage
. If the upload fails, it returns an error message.Once the image is successfully uploaded, a new entry object (
newEntry
) is created, where thecover
field references the uploaded image's ID.To create the new entry, a
POST
request is sent to the/api/foods
endpoint using the new entry data. If there is an error, it returns the error message. Otherwise, a success message is returned.
Update the UploadAtEntryCreation
Component
In order to use the server action above, we have to modify the ./src/components/UploadAtEntryCreation.tsx
file.
`js
// ./src/components/UploadAtEntryCreation.tsx
"use client";
import { uploadAtEntryCreationAction } from "@/app/actions";
import { useFormState } from "react-dom";
import { useEffect, useRef } from "react";
import SubmitButton from "./SubmitButton";
import { toast } from "react-toastify";
const initialState = {
uploadError: null,
uploadSuccess: null,
};
export default function UploadAtEntryCreation() {
const [state, formAction] = useFormState(
uploadAtEntryCreationAction,
initialState
);
const formRef = useRef(null);
useEffect(() => {
formRef?.current?.reset();
toast.success(state?.uploadSuccess);
}, [state]);
return (
ref={formRef}<br>
action={formAction}<br>
className="flex rounded h-screen lg:w-full"<br>
><br>
<br>
<br>
<p><br>
Upload a File at Entry Creation<br>
</p><br>
<span><br>
Here, you can create an entry with an image!<br>
</span><br>
<br>
<br>
Food Name<br>
type="text"<br>
name="name"<br>
placeholder="Name"<br>
className="text-sm text-[#71717a] p-2 border"<br>
/><br>
<span><br>
Select the image for this entry!<br>
</span><br>
<br>
<br>
<span><br>
Cover<br>
type="file"<br>
name="files"<br>
className="text-sm text-[#71717a] p-5 lg:p-0 border"<br>
/><br>
<span><br>
Here, you can upload two or more files!<br>
</span><br>
</span><br>
{state?.uploadError ? (<br>
<span>{state?.uploadError}</span><br>
) : null}<br>
<br>
<br>
<br>
<br>
);
}
`
- The code above uses the
useFormState
hook to manage form state and handle submission via theuploadAtEntryCreationAction
server action. TheinitialState
initializesuploadError
anduploadSuccess
tonull
. -
useEffect
anduseRef
: TheformRef
references the form to reset it after a successful upload. TheuseEffect
hook triggers this reset whenstate.uploadSuccess
changes, and displays a success toast. Render: The form collects a "Food Name" using the input name as
name
and an image file using the input type asfile
and name asfiles
. When the form is submitted, it triggers theuploadAtEntryCreationAction
server action to handle the file upload and entry creation process.And the
SubmitButton
button component handles the form submission.
This is what we should see when we perform this operation:
View, Delete, and Update Images
Now, we want to be able to click on the "Gallery" tab and see all the images that have been uploaded to our Strapi backend.
Here is what we want to do in this part of this tutorial:
- Create a modal that will allow us edit any of the images. We could edit the image name (
name
), caption (caption
) and alternative text (alternativeText
). This will trigger theupdateImageAction
server action. - Show a list of all the images uploaded to the Strapi backend using the
fetchImage
service. - And a delete function that will call the
deleteImage
service.
Update Image Update Server Action
To update an image in Strapi, we need to update the image fileInfo
field, just as we will see below. Locate the ./src/app/actions.tsx
and update the updateImageAction
server action with the following code:
`js
// ./src/app/actions.tsx
...
export async function updateImageAction(prevState: any, formData: FormData) {
try {
// get data object from form-data
const data = Object.fromEntries(formData);
// creat new image data
const newImageData = {
name: data.name || prevState.imageSelected.name,
alternativeText:
data.alternativeText || prevState.imageSelected.alternativeText,
caption: data.caption || prevState.imageSelected.caption,
};
// image ID from form-data
const imageId = data.imageId;
// append `fileInfo` to form-data
const form = new FormData();
form.append("fileInfo", JSON.stringify(newImageData));
const response = await fetch(`${STRAPI_URL}/api/upload?id=${imageId}`, {
method: "post",
body: form,
});
// image update result
const result = await response.json();
if (result.error) {
return {
uploadError: result.error.message,
uploadSuccess: null,
};
}
return {
uploadError: null,
uploadSuccess: "Image info updated successfully",
};
} catch (error: any) {
return {
uploadError: error.message,
uploadSuccess: null,
};
}
}
`
In the updateImageAction
server action above, we have the following:
- It takes parameters
prevState
andformData
.prevState
, which is the previous state, containing the currently selected image’s data.formData
is the form data containing updates to be applied to the image. It should containname
,caption
andalternativeText
from the update image form modal below. - We extract the new image information from formData using
Object.fromEntries(formData)
. - We set
newImageData
with either the new values or existing values fromprevState
. - Appends
newImageData
as JSON to a FormData object for submission. - Sends a POST request to the Strapi upload API endpoint
/api/upload?id={imageId}
to update the image data, using the image ID (imageId
) from data. - If an error occurs, it returns an error message; otherwise, it confirms a successful update.
### Create a Modal Form for Image Update
Inside the
./src/components/forms
component, create a file calledUpdateImageModal.tsx
and add the following code:
`js
// ./src/components/forms/UpdateImageModal.tsx
"use client";
import { toast } from "react-toastify";
import SubmitButton from "../SubmitButton";
import { useFormState } from "react-dom";
import { updateImageAction } from "@/app/actions";
import { ImageEntry } from "@/app/Types";
// Define the props interface for UpdateImageModal
interface UpdateImageModalProps {
closeModal: () => void;
imageSelected: ImageEntry;
handleFetchImages: () => Promise;
}
export function UpdateImageModal({
closeModal,
imageSelected,
handleFetchImages,
}: UpdateImageModalProps) {
const initialState = {
uploadError: null,
uploadSuccess: null,
imageSelected,
};
const [state, formAction] = useFormState(updateImageAction, initialState);
if (state?.uploadSuccess) {
toast.success(state?.uploadSuccess);
handleFetchImages();
closeModal();
}
return (
onClick={() => {
closeModal();
}}
className="relative w-fit border bg-black text-white px-3 py-1 rounded-md"
>
close
Update File Infos: {imageSelected.name.split(".")[0]}{" "}
Update a specific image in your application.
Name
defaultValue={imageSelected.name?.split(".")[0]}
placeholder={imageSelected.name?.split(".")[0]}
type="text"
name="name"
className="text-sm text-[#71717a] p-5 lg:p-2 border"
/>
Type in a new name
<div className="flex flex-col space-y-2">
<label htmlFor="cover">Caption</label>
<input
defaultValue={imageSelected?.caption?.split(".")[0]}
placeholder={imageSelected?.caption?.split(".")[0]}
type="text"
name="caption"
className="text-sm text-[#71717a] p-5 lg:p-2 border"
/>
<span className="text-sm text-[#71717a]">Give it a caption</span>
</div>
<div className="flex flex-col space-y-2">
<label htmlFor="">Alternative Text</label>
<input
defaultValue={imageSelected?.alternativeText?.split(".")[0]}
placeholder={imageSelected?.alternativeText?.split(".")[0]}
type="text"
name="alternativeText"
className="text-sm text-[#71717a] p-5 lg:p-2 border"
/>
<span className="text-sm text-[#71717a]">
Create an alternative text for your image
</span>
</div>
<input type="hidden" name="imageId" value={imageSelected?.id} />
{state?.uploadError ? (
<span className="text-red-500">{state?.uploadError}</span>
) : null}
<SubmitButton title="Update" />
</div>
</form>
</div>
</div>
);
}
`
In the modal form above, here is what we did:
- This modal component above provides a form to update the selected image's metadata or file info (name, caption, and alternative text).
- It takes the props
closeModal
function to close the modal.imageSelected
, which is the currently selected image for editing.handleFetchImages
function to refresh the list of images after an update. - It has the states
useFormState
withupdateImageAction
to manage form submissions and handle success/error feedback. - It displays form fields with placeholders
name
,caption
andalternativeText
and default values based onimageSelected
. - Calls
handleFetchImages()
andcloseModal()
when the update is successful. - Shows error messages if any occur during the update. Provides a "close" button to exit the modal without updating.
Import UpdateImageModal
and Add Delete Function inside the Gallery Component
Now, update the Gallery component to include a delete function and the form modal we created above.
`js
// ./src/components/Gallery.tsx
"use client";
import { useEffect, useState, useRef } from "react";
import Image from "next/image";
import { UpdateImageModal } from "./form/UpdateImageModal";
import { deleteImage, fetchImages } from "@/app/services";
import { toast } from "react-toastify";
import { ImageEntry } from "@/app/Types";
const STRAPI_URL: string = "http://localhost:1337";
export default function Gallery() {
const [images, setImages] = useState([]);
const [selectedImage, setSelectedImage] = useState(null);
const [update, setUpdate] = useState(false);
const handleFetchImages = async () => {
const images = await fetchImages();
setImages(images);
};
const closeModal = () => {
setUpdate(false);
};
const onDeleteImage = async (imageId: number) => {
await deleteImage(imageId);
const newImages = [...images].filter((image) => image.id !== imageId);
setImages(newImages);
toast.success("Image Deleted");
};
useEffect(() => {
handleFetchImages();
}, []);
return (
{update ? (
closeModal={closeModal}
imageSelected={selectedImage as ImageEntry}
handleFetchImages={handleFetchImages}
/>
) : null}
<div className="w-full my-5">
<p className="text-lg font-semibold">Update Image Info</p>
<span className="text-sm text-[#71717a]">
This is where you find all uploaded images so as update anyone.
</span>
</div>
{images?.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-5 w-full pt-10">
{images?.map((image) => (
<div className=" group border">
<div className="h-[200px]">
<Image
src={`${STRAPI_URL}${image?.url}`}
alt={image.name}
width={400}
height={300}
className="transition-all duration-300 opacity-100 h-full w-full max-w-full rounded-lg group-hover:opacity-50"
/>
</div>
<div className="flex flex-col gap-y-2 border">
<p className="flex justify-center items-center text-sm text-center text-[#71717a] h-10">
{image.name.split(".")[0]?.length > 10
? image.name.split(".")[0].slice(0, 10) + "..."
: image.name.split(".")[0]}
</p>
<button
className="bg-black text-white px-3 py-1 text-sm rounded-md"
onClick={() => {
setUpdate(true);
setSelectedImage(image);
}}
>
Update
</button>
<button
onClick={() => {
onDeleteImage(image.id);
}}
className="bg-red-500 text-white px-3 py-1 rounded-md text-sm"
>
Delete
</button>
</div>
</div>
))}
</div>
)}
{images?.length <= 0 && (
<p className="w-full text-orange-300 pt-5">No Images in Gallery.</p>
)}
</div>
);
}
`
Here is what the Gallery
component above does:
- It displays all uploaded images and allows for updating or deleting individual images.
- It has the states
images
, which stores the fetched images,selectedImage
, which stores the currently selected image for editing, andupdate
, which controls the visibility of theUpdateImageModal
which we imported. - It has the functions
handleFetchImages
that fetches the list of images from the Strapi API,closeModal
, which closes theUpdateImageModal
and theonDeleteImage
, which deletes an image by ID, then updates the state to remove the deleted image from the view. - It displays a grid of images, each with an update and delete button.
- Opens
UpdateImageModal
when "Update" is clicked and setsselectedImage
. - Displays a message if no images are available in the gallery.
Update Image Demo
Let's see how our app works when we try to update an image.
Delete Image Demo
Here is what deleting an image looks like:
Upload Image from an API controller in Strapi
What happens when we want to have a custom route that should upload an image to Strapi? This is where Strapi backend customization comes in.
For this part, we want to create a controller that will respond to a POST
request on the route favorite-food
. So, we need to create a custom controller and a custom route.
Create a Custom Route for Upload
Inside the ./src/API/food/routes
folder, create a new file called favorite-food.ts.
This will be our new custom route. Add the code below.
`js
// ./src/api/food/routes/favorite-food.ts
// Create route to post favorite food
export default {
routes: [
{
// Path defined with a URL parameter
method: "POST",
path: "/foods/favorite-food",
handler: "api::food.food.uploadImage",
},
],
};
`
In the code above, we specified an HTTP POST
method that will handle requests made to /foods/favorite-food
route. And we specified that controller uploadImage
, which we will create soon, should handle this request. Let's proceed and create the controller.
Create Custom Controller for Upload
Locate the ./src/api/food/controllers/food.ts
and replace it with the following code:
`js
// ./src/api/food/controllers/food.ts
/**
- food controller */
import { factories } from "@strapi/strapi";
export default factories.createCoreController(
"api::food.food",
({ strapi }) => ({
// create custom controller
async uploadImage(ctx) {
const contentType = strapi.contentType("api::food.food");
// get files
const { files } = ctx.request;
// name of input (key)
const file = files["anyName"];
// create image using the upload plugin
const createdFiles = await strapi.plugins.upload.services.upload.upload({
data: {
fileInfo: {
name: file["originalFilename"], // set the original name of the image
caption: "Caption", // give it a caption
alternativeText: "Alternative Text", // give it an alternative text
},
},
files: file,
});
// send response
ctx.status = 200;
ctx.body = {
data: createdFiles,
};
},
})
);
`
In the code above, we created a custom controller called uploadImage
. It gets the files from the request context sent from the POST
request. And then we get the file
, which could be a single or multiple of them using the input key, from the request context. After that, we used the upload plugin and the upload
method to upload the image or images. Finally, we sent back a response with a 200
HTTP status and the created file or files (createdFiles
) as data.
👋 NOTE
You can upload single or multiple images using the example above.
Now, let's test this with Postman.
Create Custom Controller for Upload
From the image above, we can see that we set the name of the input as anyName
. This could be any name of your choice. But it has to correspond with the const file = files["anyName"];
we specified in the controller above.
We also selected two image files and then clicked send. The response is returned with the created files in the array data
.
Image Upload to Strapi using Postman
Now, we want to perform what we have done using Next.js with Postman. Here are the things we will learn in this section.
- Single or Multiple Upload.
- Link an Image to an Entry by Upload.
- Link an already uploaded image to an entry using its ID.
- Delete an image.
- Update an image.
1. Single or Multiple Upload
To perform this, we need to make a POST
request to the endpoint /api/upload
as form-data. The input name should be files
.
Make sure to set the request body as form data. Add the key as files,
select the type as a file, and then select an image to upload.
See the image below:
Single or Multiple Upload to Strapi via Postman
2. Link an Image to an Entry by Upload
In this part, we will upload and at the same time link it to an entry (food entry).
Here, we will make a POST
request to /api/upload
with the following keys or input values:
-
files
: The image file we want to link to an entry. -
ref
: This is the reference type for Food collection. And this should beapi::food.food
. -
refId
: Theid
of the food entry. Get an entry ID for your Strapi Food collection. -
field
: The field that will hold the image in the Food collection. This should becover
.
Set the body of the request as form data, add the keys above, and click send.
See the image below:
Link image to an entry by upload
3. Link an already uploaded image to an entry
How about we link an image existing in our Strapi backend to and entry that is also existing in our Strapi backend? Let's do this using the following steps.
- Get the
id
of the image you want to link. To do this, make aGET
request to the endpoint/api/upload/files
. - Get the
documentId
of the food entry to which you want to link the image. Make aGET
request to the endpoint/api/upload/files
to get thedocumentId
. - Finally, make a
PUT
request to the endpoint/api/foods/{documentId}
, wheredocumentId
is the doucment ID of the food entry. - The request should be a JSON request with the following structure:
js
{
"data": {
"cover": 13
}
}
See the image below
Link an Image to an entry by its ID
4. Delete an Image
To delete an image, we will have to make a DELETE
request to /api/upload/files/${imageId}
, where imageId
is the ID of the image we want to delete.
See image below:
Delete an image in Strapi via Postman
5. Update an Image File Information in Strapi
Updating an image will require that we make a PUT
request to the endpoint /api/upload?id=${imageId}
. The imageId
here represents the ID of the image we want to update.
We will send our request as JSON using the structure below to update the image file information:
js
{
"fileInfo": {
"name": "new image",
"caption": "a caption",
"alternativeText": "the alternative text"
}
}
See Image below:
Update an Image File Information in Strapi
Conclusion
In this tutorial, we have learnt about image upload to Strapi via REST API using Next.js and Postman. We covered single and multiple upload, linking an image by id or upload to an entry, upload an image upon creation and single or multiple file upload from an API controller in Strapi CMS. In the same vein, we looked at updating image information and deleting images. The vital Next.js server actions were not left behind as well.
The media library and image upload is a very significant part of any web application. With Strapi headless CMS, you can upload image seamlessly using an frontend technology of your choice. Here is the Github repo to the project for this tutorial.