How to use Firebase Storage to upload and retrieve files in Next.js (Pages Router)?
Tags: tutorial, javascript, nextjs, webdev
Published: 2023-10-31 08:16:10

In this tutorial, I'll guide you through the process of creating Next.js (Pages Router) Endpoints that allows you to upload to and retrieve files from Firebase Storage. I'll break down the code into 7 steps to make it easier to follow 👇🏻

## 1. Installation

- `firebase`: This package is needed to interact with Firebase services, including Firebase Storage.

- `uuid`: This package generates universally unique identifiers (UUIDs) which can be useful when naming files or objects in Firebase Storage.

- `formidable`: Formidable is a library for parsing form data, which can be useful when handling file uploads in your Next.js application.

```bash
npm install firebase uuid formidable
```

## 2. Create Storage Endpoint in Next.js (Pages Router)

In this code block, a Next.js API route called `storage.ts` is created. It listens to both GET and POST requests. All the other requests are returned with the status of `Bad Request`.

```typescript
// File: pages/api/storage.ts

import type { NextApiRequest, NextApiResponse } from 'next'

export default async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === 'GET') return await fileGET(req, res)
  if (req.method === 'POST') return await filePOST(req, res)
  return res.status(400).end()
}

// Disable parsing the body by Next.js default behavior
export const config = {
  api: {
    bodyParser: false,
  },
}
```

## 3. Initialize Firebase App

```typescript
// File: pages/api/storage.ts

import { initializeApp } from 'firebase/app'
import { getStorage } from 'firebase/storage'
import fireBaseConfig from '@/lib/db/firebaseConfig'

async function filePOST(request: NextApiRequest, res: NextApiResponse) {
  // Initialize the Firebase app with the provided configuration
  const app = initializeApp(fireBaseConfig)
  // Get a reference to the Firebase Storage and parse the request data as a FormData object
  const storage = getStorage(app)
  // More code to handle uploads incoming...
}
```

In this code block, we are initializing the Firebase app and setting up Firebase Storage to handle file uploads. Here's what's happening:

- `import fireBaseConfig from './firebaseConfig'`: This line imports the Firebase configuration from firebaseConfig.(js/ts) file. **Make sure the config has `storageBucket: 'gs://...appspot.com'` key**.

- `const app = initializeApp(fireBaseConfig)`: Initializes the Firebase app using the provided configuration. It sets up the connection to your Firebase project.

- `const storage = getStorage(app)`: Gets a reference to the Firebase Storage using the initialized app. This allows us to interact with Firebase Storage to upload and retrieve files.

## Parse the incoming fields and files in the Form's Submission

### 4. Set up formidable

```typescript
// File: lib/storage/formidable.ts

import { Writable } from 'stream'
import formidable from 'formidable'

export const formidableConfig = {
  maxFields: 7,
  multiples: false,
  keepExtensions: true,
  allowEmptyFiles: false,
  maxFileSize: 10_000_000,
  maxFieldsSize: 10_000_000,
}

export function formidablePromise(req: NextApiRequest, opts?: Parameters<typeof formidable>[0]): Promise<{ fields: formidable.Fields; files: formidable.Files }> {
  return new Promise((accept, reject) => {
    const form = formidable(opts)
    form.parse(req, (err, fields, files) => {
      if (err) {
        return reject(err)
      }
      return accept({ fields, files })
    })
  })
}

export const fileConsumer = <T = unknown>(acc: T[]) => {
  const writable = new Writable({
    write: (chunk, _enc, next) => {
      acc.push(chunk)
      next()
    },
  })
  return writable
}
```

In this code block, we are setting up `formidable` parsing form data, including file uploads. Here's what's happening:

- `formidableConfig`: This object defines configuration options for formidable. For example, it limits the number of fields, allows only single files (no multiples), keeps file extensions, and sets maximum file and fields sizes.

- `formidablePromise`: This function is a Promise-based wrapper around formidable. It takes a NextApiRequest and optional options and returns a promise that resolves to an object containing fields and files parsed from the request.

- `fileConsumer`: This function creates a writable stream to collect the file's data in chunks. It's used to handle file uploads in the next section.

### 5. Using formidable, obtain the files and perform checks

```typescript
// File: pages/api/storage.ts

import { formidableConfig, formidablePromise, fileConsumer } '../../lib/storage/formidable'

const chunks: never[] = []
const { fields, files } = await formidablePromise(request, {
    ...formidableConfig,
    fileWriteStreamHandler: () => fileConsumer(chunks),
})
const file = files.file
const fileBuffer = Buffer.concat(chunks)
if (!file || !file[0]) {
    return res.status(400).json({ error: 'No File Provided' })
}
if (file[0].size > 5 * 1024 * 1024) {
    return res.status(400).json({ error: 'File size exceeds the limit of 5 MB.' })
}
```

In this code block, we are deferring the file upload handling to the `formidable` setup and performing checks. Here's what's happening:

- `file` is extracted from the files object. `file` here is an array of items that came into the form.

- `fileBuffer` is created by concatenating the collected file data chunks. This buffer can be used for further processing or uploading to Firebase Storage.

- Error checks are performed:

   - If there is no file provided or if the file array is empty, it responds with a 400 status and an error message.
   - If the file size exceeds 5 MB, it responds with a 400 status and a size limit error message.


## 6. Create a unique ID and upload to Firebase Storage

```typescript
// File: pages/api/storage.ts

import { v4 as uuidv4 } from 'uuid'
import { getStorage, ref, uploadBytes } from 'firebase/storage'

try {
  const fileId = uuidv4()
  const storageRef = ref(storage, `uploads/${fileId}/${file[0].originalFilename}`)
  const { metadata } = await uploadBytes(storageRef, fileBuffer)
  const { fullPath } = metadata
  if (!fullPath) {
    return res.status(403).json({
      error: 'There was some error while uploading the file.',
    })
  }
  const fileURL = `https://storage.googleapis.com/${storageRef.bucket}/${storageRef.fullPath}`
  console.log(fileURL)
  // Save the fileURL somewhere (say Redis DB) to retrieve the actual file using this as the identifier
  return res.status(200).json({ message: 'Uploaded Successfully' })
} catch (e: any) {
  const tmp = e.message || e.toString()
  console.log(tmp)
  return res.status(500).send(tmp)
}
```

In this code block, we are uploading to Firebase Storage via their SDK. Here's what's happening:

- `uuidv4()`: This function generates a unique UUID using the uuid library. It creates a unique identifier for the uploaded file, which can be used in the storage path.

- `storageRef`: It creates a reference to the storage path where the file will be stored in Firebase Storage. The path is constructed using the generated unique ID and the original filename of the uploaded file.

- `uploadBytes`: This function uploads the file data (fileBuffer) to Firebase Storage using the storageRef. It returns metadata about the uploaded file.

- `fileURL`: The code constructs a public URL for the uploaded file using the Firebase Storage URL format.

- If the file is successfully uploaded, it responds with a 200 status and the public file URL. Additionally, it suggests saving this URL for future retrieval.

- If any error occurs during the upload process, it logs the error message and responds with a 500 status code.

## 7. Retrieve the file from Firebase Storage using the identifier

```typescript
// File: pages/api/storage.ts

import { getDownloadURL, getStorage, ref } from 'firebase/storage'

async function fileGET(request: NextApiRequest, res: NextApiResponse) {
  // Extract the 'file' parameter from the request URL.
  const file = request.query.file
  // Check if the 'image' parameter exists in the URL.
  if (file && typeof file === 'string') {
    try {
      // Initialize the Firebase app with the provided configuration.
      const app = initializeApp(fireBaseConfig)
      // Get a reference to the Firebase storage.
      const storage = getStorage(app)
      // Create a reference to the specified file in storage.
      const fileRef = ref(storage, file)
      // Get the download URL of the file.
      const filePublicURL = await getDownloadURL(fileRef)
      // Return a JSON response with the file's public URL and a 200 status code.
      return res.status(200).json({ filePublicURL })
    } catch (e: any) {
      // If an error occurs, log the error message and return a JSON response with a 500 status code.
      const tmp = e.message || e.toString()
      console.log(tmp)
      return res.status(500).send(tmp)
    }
  }
  // If the 'file' parameter is not found in the URL, return a JSON response with a 400 status code.
  return res.status(400).json({ error: 'Invalid Request' })
}
```

In this code block, a request is made to retrieve a file from Firebase Storage using an identifier. Here's what's happening:

- It extracts the `file` parameter from the request URL, which should be the public URL received when upload was performed.

- The code then uses `getDownloadURL` to obtain the actual URL of the file.

- Checks performed:

   - If the 'file' parameter is missing or invalid, it responds with a 400 status code and an error message indicating an invalid request.

   - If the file is successfully retrieved, it responds with a 200 status and the file's public URL.

   - If any error occurs, it logs the error message and responds with a 500 status code.

## You're Done!

In this tutorial, we've set up a Next.js project to interact with Firebase Storage for file uploads and retrievals. We've used formidable to parse form data, generated unique IDs for uploaded files, and ensured file size limits. For retrieval, we've used Firebase Storage's public URLs.

Happy coding!