Skip to content

Upload and Display Images Using Prisma

Sometimes you need to handle and manage images linked to information that’s stored in the database of your web application. We can improve and simplify both image management and data storage using two helpful products: Prisma and Cloudinary. In this article, we’ll create a Next.js app to upload the images and display them as a gallery. To handle image upload, optimization, storage, and display, we’ll use a Cloudinary account. To store image details, we’ll use Prisma with a local SQLite database.

Prisma is an open-source ORM (object-relational mapping) for Node.js and TypeScript. It has a module to define the data models that will be used by the app, and their relationships, based on a Prisma Schema. The tool connects to a database (MySQL, PostgreSQL, SQLite) to store information related to the data models. There’s also a Prisma Client module that offers an auto-generated query builder.

To keep it simple, we’ll be uploading images to Prisma like this:

  • Cloning your repository and installing dependencies.
  • Configure your environment variables.
  • Start the app.
  • Create an API endpoint.
  • Update your frontend to handle image upload.

      Cloudinary is a cloud-based service for managing media assets. You can upload, store, and manipulate images and videos. As Cloudinary is offered as a software as a service (SaaS), it has servers that handle the upload, process, and delivery of media assets, with the possibility of expansion if your application’s traffic increases.

      Next.js is a popular React framework used to create user interfaces, static pages, and server-side rendered pages, and then combine them in the same application. It allows developers to create server-less API routes using Node.js logic.

      Let’s create an image gallery app with two pages: one to upload images, and another to list all the existing pictures. We’ll upload the image files to Cloudinary servers, and we’ll store related details in a SQLite local database using Prisma. For the user interface and the server-side logic, we’ll use Next.js and some NPM packages to make our work easier.

      AWS and S3 Integration

      To further enhance our image storage capabilities, we can also integrate AWS S3. Here’s how:

      1. Set up an AWS account: Before you can use AWS services, you need to set up an AWS account.
      2. Create an IAM user: For security reasons, it’s recommended to create an IAM user with the necessary permissions rather than using the root account.
      3. Set up an S3 bucket: AWS S3 provides scalable object storage. Create an S3 bucket where you’ll store the uploaded images.
      4. Update your Prisma schema: Modify your Prisma schema to store the image’s S3 bucket path.
      5. Build the image upload component: This component will allow users to select and upload images.
      6. Build the image upload service: This service will handle the backend logic for uploading images to S3.
      7. Integrate the component and service: Finally, integrate the component and service into your application to allow users to upload images to S3.

      It’s worth noting that while Cloudinary is excellent for image management, it’s a best practice to store images in blob storage, like AWS S3, rather than directly in the database. By using S3, you can store the image and then save the image’s S3 bucket path in your database, be it SQLite, MySQL, PostgreSQL, or another database of your choice.

      If you want to see the app live, here’s a working CodeSandbox:

      Or you can go to https://prisma-upload-image.vercel.app.

      If you want to see and edit the code of the app, you can clone https://github.com/fgiuliani/prisma-upload-image

      To run the app locally, you’ll need Node.js installed on your computer and a Cloudinary account.

      After cloning the repository, replace the values of environment variables present in .env.local file:

      • CLOUD_NAME
      • API_KEY
      • API_SECRET

      You can find the three values at the top left corner of your Cloudinary Dashboard page.

      Cloudinary Dashboard page

      • DATABASE_PROVIDER: The database provider we want Prisma to connect and use. For this example, we’ll use a SQLite local database. You can use a MySQL or PostgreSQL database if you want, too.

      • DATABASE_URL: The URL of the database you’ll use.

      • SERVER_PATH: The root path of your application. This is needed to support Next.js API routes.

      You’ll also need to use the Terminal to run npm install in the directory where the code of the app lives, so all the involved NPM packages are downloaded and installed.

      The repository already contains a SQLite database file with the schema used by Prisma to handle the data and generate the queries. If you edit or add models in schema.prisma file, you’ll need to run npx prisma db push –preview-feature in the Terminal in order to sync the database with the Prisma schema.

      To run the app, go to the Terminal and run npm run dev.

      Let’s see how the app optimizes and uploads images using Cloudinary in utils/cloudinary.js

      const cloudinary = require("cloudinary").v2;
      
      cloudinary.config({
        cloud_name: process.env.CLOUD_NAME,
        api_key: process.env.API_KEY,
        api_secret: process.env.API_SECRET
      });
      
      export function uploadImage(imageUploaded) {
        return new Promise((resolve, reject) => {
          cloudinary.uploader.upload(
            imageUploaded,
            { width: 400, height: 300, crop: "fill" },
            (err, res) => {
              if (err) reject(err);
              resolve(res);
            }
          );
        });
      }
      Code language: JavaScript (javascript)

      1 – We’ll configure the API Key, Secret, and Cloud Name for the Cloudinary integration. 2 – We’ll call the Cloudinary Uploader upload method. 3 – The first parameter of the method is the image we want to upload. cloudinary.uploader.upload method can receive the image in different ways: using local file path, the image data (byte array buffer), the data URI, a remote address, or a private storage bucket URL. 4 – The second parameter is an options object. We’ll use it to define some options and modify the image before uploading it to Cloudinary’s servers.

      As we’ve defined a schema for the database, we can now use Prisma Client to build queries in a simple way. We’ll instantiate the client in utils/prisma.js.

      import { PrismaClient } from "@prisma/client";
      
      const prisma = new PrismaClient();
      export default prisma;
      Code language: JavaScript (javascript)

      We’ll use it to save information in the database, in pages/api/upload.js.

      import { getImage } from "../../utils/formidable";
      import { uploadImage } from "../../utils/cloudinary";
      import prisma from "../../utils/prisma";
      
      export const config = {
        api: {
          bodyParser: false,
        },
      };
      
      export default async function handle(req, res) {
        const imageUploaded = await getImage(req);
      
        const imageData = await uploadImage(imageUploaded.path);
      
        const result = await prisma.image.create({
          data: {
            publicId: imageData.public_id,
            format: imageData.format,
            version: imageData.version.toString(),
          },
        });
      
        res.json(result);
      }
      Code language: JavaScript (javascript)

      We’ll create an image with details extracted from the Cloudinary upload response. Then, we’ll use those details later to conform the image public URL and display it in the gallery page.

      Another cool package that we’re using in the app is formidable, which helps us handle the form submitted by the user when uploading the image, so we can generate an image buffer to be sent through the Cloudinary API. We’ll extract an image buffer from the form data in utils/formidable.js

      import { IncomingForm } from "formidable";
      
      export async function getImage(formData) {
        const data = await new Promise(function (resolve, reject) {
          const form = new IncomingForm({ keepExtensions: true });
          form.parse(formData, function (err, fields, files) {
            if (err) return reject(err);
            resolve({ fields, files });
          });
        });
      
        return data.files.image;
      }
      Code language: JavaScript (javascript)

      We can also see how we’ll handle image uploads in the page, using an <input> HTML element, in pages/upload.jsx

      import React, { useState } from "react";
      import Layout from "../components/Layout";
      import Router from "next/router";
      
      const Upload = () => {
        const [imageUploaded, setImageUploaded] = useState();
      
        const handleChange = (event) => {
          setImageUploaded(event.target.files[0]);
        };
      
        const submitData = async (e) => {
          e.preventDefault();
      
          if (!imageUploaded) {
            return;
          }
      
          try {
            const formData = new FormData();
            formData.append("image", imageUploaded);
      
            await fetch("/api/upload", {
              method: "POST",
              body: formData
            });
      
            Router.push("/");
          } catch (error) {
            console.error(error);
          }
        };
      
        return (
          <Layout>
            <div className="page">
              <form onSubmit={submitData}>
                <h1>Upload Image</h1>
      
                <input
                  onChange={handleChange}
                  accept=".jpg, .png, .gif, .jpeg"
                  type="file"
                ></input>
      
                <input type="submit" value="Upload" disabled />
              </form>
            </div>
            <style jsx>{`
              .page {
                background: white;
                padding: 3rem;
                display: flex;
                justify-content: center;
                align-items: center;
              }
      
              input[type="submit"] {
                background: #ececec;
                border: 0;
                padding: 1rem 2rem;
              }
            `}</style>
          </Layout>
        );
      };
      
      export default Upload; 
      Code language: JavaScript (javascript)

      1 – We’ll limit the allowed file formats that our <input> element will support to only accept image files. 2 – Whenever the <input> changes its value, we’ll update the state of imageUploaded to contain the most recent file uploaded details. 3 – After clicking Submit, we’ll create a FormData object and include the uploaded image. 4 – We’ll send that object as a parameter when we call the pages/api/upload.js API route.

      In pages/api/images.js we’ll retrieve all the image records stored in the database.

      import prisma from "../../utils/prisma";
      
      export default async function handle(req, res) {
        const images = await prisma.image.findMany();
        res.json(images);
      }
      Code language: JavaScript (javascript)

      Next, we’ll can generate an *<img>* HTML element for each image in pages/index.jsx

      import Layout from "../components/Layout";
      
      const Gallery = (props) => {
        return (
          <Layout>
            <div className="page">
              <h1>Image Gallery</h1>
              <main>
                {props.images.map((image) => (
                  <img
                    src={`https://res.cloudinary.com/${process.env.CLOUD_NAME}/v${image.version}/${image.publicId}.${image.format}`}
                    key={image.publicId}
                  />
                ))}
              </main>
            </div>
            <style jsx>{`
              .image {
                background: white;
                transition: box-shadow 0.1s ease-in;
              }
      
              .image:hover {
                box-shadow: 1px 1px 3px #aaa;
              }
      
              .image + .image {
                margin-top: 2rem;
              }
            `}</style>
          </Layout>
        );
      };
      
      export const getServerSideProps = async () => {
        const res = await fetch(`${process.env.SERVER_PATH}/api/images`);
        const images = await res.json();
        return {
          props: { images },
        };
      };
      
      export default Gallery;
      Code language: JavaScript (javascript)

      With this image gallery app, we learned how to upload images to Cloudinary using its Node.js SDK, how to display images stored in Cloudinary servers, and how to store details related to them in a database using Prisma ORM. This example app can be used and extended for more advanced and complex use cases, to add other models to the Prisma schema, or to manipulate the files stored in Cloudinary.

Back to top

Featured Post