How to Upload and Retrieve Images to and From MongoDB Using NextJS 13 App Router
Jasser Mark Arioste
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:
- Create an
<ImageUploader/>
component to select and upload files from the user's computer - Connect to MongoDB and Initialize GridFS
- Create an API
/api/upload
as an endpoint to upload files or images using the NextJS app router. - 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:
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
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