ReactHustle

How to Upload and Retrieve Images to and From MongoDB Using NextJS 13 App Router

Jasser Mark Arioste

Jasser Mark Arioste

How to Upload and Retrieve Images to and From MongoDB Using NextJS 13 App Router

Hello, hustlers! In this tutorial, you'll learn how to upload and retrieve images to and from MongoDB using GridFS and NextJS 13 app router.

Introduction #

NextJS has recently released version 13.4 which marks the /app router as stable. Vercel also recently unveiled Blob storage which is currently in private beta.

With this new combination of app router and file storage, I considered using alternatives such as MongoDB GridFS. Questions suddenly popped into my head: How do you now upload and retrieve images to MongoDB? Is it harder or easier than using the /pages directory?

In my research, the answer is that it's definitely easier and the code is much cleaner and readable than before.  If you're using MongoDB as your database, I think this tutorial will help you.

Tutorial Objectives #

In this tutorial, we'll do the following:

  1. Create an <ImageUploader/> component to select and upload files from the user's computer
  2. Connect to MongoDB and Initialize GridFS
  3. Create an API /api/upload as an endpoint to upload files or images using the NextJS app router. 
  4. Create an API /api/uploads/[filename] as an endpoint to retrieve files or images using the NextJS app router.

All right, let's start with the implementation.

Prerequisites #

I assume you have basic knowledge of NextJS and you have a MongoDB server up and running on your local environment or remote.

Step 0 - Project Setup #

Creating a NextJS Project

Let's quickly create a new NextJS 13 project by running the command in your terminal:

npx create-next-app --ts --app --tailwind next-js-upload-files-example

Explanation:

The --ts option makes sure that our NextJS app is set up with Typescript.

The --app option generates the /app directory instead of the /pages directory

The --tailwind option automatically sets up tailwind CSS into our NextJS app.

After that, you can use cd next-js-upload-files-example && yarn dev to start the local dev server.

Step 1 - Adding the Environment Variables #

Next, let's add the environment variables that we'll be using in our project. First, create the file .env.local on your root project directory and add the MONGODB_URI variable.

# .env.local
# MONGODB_URI=mongodb://localhost:27017/mydb
MONGODB_URI="Your MongoDB connection string"
12

Step 2 - Installing Dependencies #

Let's install the dependencies that we'll be using by running the command:

yarn add axios mongodb
1

Explanation:

axios - We'll use axios instead of fetch because I find it easier to use this for file upload. You can easily track the upload progress by defining an onUploadProgress callback in the options.

mongodb - MongoDB driver to connect to the database and initialize GridFS

Step 1 - Creating the <ImageUploader/> component #

Next, we'll create a simple component to upload the files selected from a user's computer. Create the file components/ImageUploader.tsx and copy the code below:

"use client";

import axios from "axios";
import React, { FormEvent, useRef, useState } from "react";

const ImageUploader = () => {
  // 1. add reference to input element
  const ref = useRef<HTMLInputElement>(null);

  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    // 2. get reference to the input element
    const input = ref.current!;

    // 3. build form data
    const formData = new FormData();
    for (const file of Array.from(input.files ?? [])) {
      formData.append(file.name, file);
    }

    // 4. use axios to send the FormData
    await axios.post("/api/upload", formData);
  };
  return (
    <>
      <form onSubmit={handleSubmit}>
        <input type="file" name="files" ref={ref} multiple />
        <button
          type="submit"
          className="px-2 py-1 rounded-md bg-violet-50 text-violet-500"
        >
          Upload
        </button>
      </form>
    </>
  );
};

export default ImageUploader;
12345678910111213141516171819202122232425262728293031323334353637383940

The explanation is stated in the comments.

Next, let's modify the app/page.tsx file to use this component:

// app/page.tsx
import ImageUploader from "@/components/ImageUploader";

export default function Home() {
  return (
    <main className="p-10">
      <ImageUploader />
    </main>
  );
}
12345678910

After this step, you should have an output like this:

NextJS MongoDB image Uploader Component

Step 2 - Connecting to MongoDB #

Next, let's create some utility to connect to MongoDB and initialize a GridFS bucket. First, create the file lib/mongo.ts and copy the code below.

// lib/mongo.ts
import { MongoClient, GridFSBucket } from "mongodb";
declare global {
  var client: MongoClient | null;
  var bucket: GridFSBucket | null;
}

const MONGODB_URI = process.env.MONGODB_URI;
if (!MONGODB_URI) {
  throw new Error(
    "Please define the MONGODB_URI environment variable inside .env.local"
  );
}

/* 
  Initializes the connection to mongodb and creates a GridFSBucket
  Once connected, it will use the cached connection.
 */
export async function connectToDb() {
  if (global.client) {
    return {
      client: global.client,
      bucket: global.bucket!,
    };
  }

  const client = (global.client = new MongoClient(MONGODB_URI!, {}));
  const bucket = (global.bucket = new GridFSBucket(client.db(), {
    bucketName: "images",
  }));

  await global.client.connect();
  console.log("Connected to the Database ");
  return { client, bucket: bucket! };
}

// utility to check if file exists
export async function fileExists(filename: string): Promise<boolean> {
  const { client } = await connectToDb();
  const count = await client
    .db()
    .collection("images.files")
    .countDocuments({ filename });

  return !!count;
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546

Step 3 - Creating the Upload API #

In this step, we'll create the file upload API endpoint. Create the file /app/api/upload/route.ts. Your directory should look like this:

/ app
  └ api
    └ upload
      └ route.ts
123

Next, copy the code below:

// app/api/upload
import { connectToDb, fileExists } from "@/lib/mongo";
import { NextResponse } from "next/server";
import { Readable } from "stream";

export async function POST(req: Request) {
  const { bucket } = await connectToDb();
  // get the form data
  const data = await req.formData();

  // map through all the entries
  for (const entry of Array.from(data.entries())) {
    const [key, value] = entry;
    // FormDataEntryValue can either be type `Blob` or `string`
    // if its type is object then it's a Blob
    const isFile = typeof value == "object";

    if (isFile) {
      const blob = value as Blob;
      const filename = blob.name;

      const existing = await fileExists(filename);
      if (existing) {
        // If file already exists, let's skip it.
        // If you want a different behavior such as override, modify this part.
        continue;
      }

      //conver the blob to stream
      const buffer = Buffer.from(await blob.arrayBuffer());
      const stream = Readable.from(buffer);

      const uploadStream = bucket.openUploadStream(filename, {
        // make sure to add content type so that it will be easier to set later.
        contentType: blob.type,
        metadata: {}, //add your metadata here if any
      });

      // pipe the readable stream to a writeable stream to save it to the database
      await stream.pipe(uploadStream);
    }
  }

  // return the response after all the entries have been processed.
  return NextResponse.json({ success: true });
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546

Explanation:

The app router uses a special file "route.ts" to indicate an API route. Then, You can easily create handlers for different HTTP Methods such as GET, POST, PATCH, and DELETE by exporting async functions of the same name. In this example, we only export the POST function. This makes the code very readable and clean. You can learn more about it in the documentation.

By using this endpoint, we can now test the upload feature to our database.

Step 4 - Creating the Retrieve Images API #

Next, let's create an endpoint to retrieve images from our MongoDB database. Create the file /app/api/uploads/[filename]/route.ts. Your folder structure should look like this now with the two endpoints:

/ app
  └ api
    └ upload
      └ route.ts
    └ uploads
      └ [filename]
        └ route.ts
123456

Next, copy the code below for the GET handler:

// app/api/uploads/[filename]/route.ts
import { connectToDb } from "@/lib/mongo";
import { NextResponse } from "next/server";

type Params = {
  params: { filename: string };
};

export async function GET(req: Request, { params }: Params) {
  // 1. get GridFS bucket
  const { bucket } = await connectToDb();

  const filename = params.filename as string;
  // 2. validate the filename
  if (!filename) {
    return new NextResponse(null, { status: 400, statusText: "Bad Request" });
  }

  const files = await bucket.find({ filename }).toArray();
  if (!files.length) {
    return new NextResponse(null, { status: 404, statusText: "Not found" });
  }
  
  // 3. get file data
  const file = files.at(0)!; 

  // 4. get the file contents as stream
  // Force the type to be ReadableStream since NextResponse doesn't accept GridFSBucketReadStream
  const stream = bucket.openDownloadStreamByName(filename) as unknown as ReadableStream;

  // 5. return a streamed response
  return new NextResponse(stream, {
    headers: {
      "Content-Type": file.contentType!,
    },
  });
}
12345678910111213141516171819202122232425262728293031323334353637

That's basically it!

Full Code and Demo #

The full code is available on my GitHub: jmarioste/next-js-13-upload-to-mongodb-tutorial

NextJS 13 Upload And Retrieve files or images to MongoDB

Conclusion #

You learned how to retrieve and upload files to MongoDB using the NextJS app router. In this example, we did not use any third-party libraries like multer or formidable to upload files which makes the code a lot cleaner.

If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on GitHub.

Resources #

Credits: Image by Pexels from Pixabay

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle