Deploy Next.js app to Firebase Functions with existing functions

Viewed 1807

Following a few tutorials like this one, I was able to deploy my Next.js app to Firebase Hosting/Functions. Here's the reduced folder structure of my project:

myApp
  node_modules // Next.js app dependencies
  .next // destination of npm run build of the Next.js project
  pages // Next.js pages
  functions
    lib // this is where the functions compile
    node_modules // functions' dependencies
    src
      app.ts
      index.ts
      other.ts
    package.json
  public // Next.js app public files
  firebase.json
  package.json
  ...

When I run firebase deploy, it compiles functions into its lib folder and deploys everything successfully.

However, when I try to visit the url of my web app, I get Error: could not handle the request. When I looked into the Firebase Functions log, I can see an error saying Error: Cannot find module 'react'. So, I added react to functions/package.json, npm install and firebase deploy successfully again. However, another error, but this time it says Cannot find module 'react-dom'.

My understanding is that the reason for this is that my Next.js app relies on all the dependencies listed in my package.json in my root folder (a reduced list below):

"dependencies": {
    "babel-plugin-prismjs": "^2.1.0",
    "firebase": "^8.8.1",
    "firebase-admin": "^9.11.0",
    "formik": "^2.2.9",
    "js-cookie": "^3.0.0",
    "next": "11.0.1",
    "next-i18next": "^8.5.5",
    "nookies": "^2.5.2",
    "prismjs": "^1.24.1",
    "react": "17.0.2",
    "react-dom": "17.0.2",
    "react-ga": "^3.3.0",
    "react-select": "^4.3.1",
    "react-spinners": "^0.11.0",
    "react-transition-group": "^4.4.2",
    "recharts": "^2.0.10",
    "styled-components": "^5.3.0",
    "yup": "^0.32.9"
  }

while my functions/package.json was, obviously, much shorter:

"dependencies": {
    "@sendgrid/mail": "^7.4.4",
    "firebase-admin": "^9.8.0",
    "firebase-functions": "^3.14.1",
    "next": "^11.0.1",
    "react": "^17.0.2"
  }

I assume the errors would have asked me to duplicate all the dependencies into the functions/package.json file, which I obviously don't want.

Alternatively, I can add firebase-functions and other relevant dependencies from functions/package.json to root/package.json, and have the next server setup in the root folder as well. However, how do I include the other existing cloud functions?

Here's how the contents of my functions/src files look:

index.ts

import * as admin from 'firebase-admin'

admin.initializeApp()

export * from './app' // next.js
export * from './other'

other.ts

import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'

exports.onUserCreate = functions.database.ref...
// my other cloud functions, such as db triggers, https functions, etc.

app.ts

import next from 'next'
import {https} from 'firebase-functions'

const server = next({
  dev: process.env.NODE_ENV !== 'production',
  conf: {distDir: '.next'}
})

const nextjsHandler = server.getRequestHandler()

exports.app = https.onRequest(async (req, res) => {
  await server.prepare()
  return await nextjsHandler(req, res)
})

Also, here's my firebase.json:

{
  "functions": {
    "predeploy": [
      "npm --prefix \"$RESOURCE_DIR\" run lint",
      "npm --prefix \"$RESOURCE_DIR\" run build"
    ]
  },
  "hosting": {
    "target": "myApp",
    "public": "public",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [{
      "source": "**",
      "function": "app"
    }]
  }
}

How do I deploy the Next.js app correctly and make sure my other cloud functions continue to work?

Thanks!

1 Answers

While you could lift the /functions directory to your project's root directory, this would mean that your other functions are unnecessarily bloated with all of your Next.js app's dependencies.

When you deploy a Cloud Function, everything in the configured deployment directory (/functions by default) is deployed - even if you don't use it. It is also deployed once for each function. So if you had a 10MB file to your functions directory, every function's deployed size would increase by 10MB. This is what I mean by "bloating" a function.

FYI: You can change the deployed functions directory by adding the following to your firebase.json file (docs):

"functions": {
  "source": "." // <- the project's root directory
}

Instead, you should make use of partial deployment. In this situation you'd move app.ts to your Next project's directory and leave the other functions that don't depend on your Next deployment in the /functions folder. If you deploy your entire project directory, you will end up deploying /functions too even though you don't use it, so you should instead move your Next front-end and functions into its own folder.

The file structure will look similar to (details below):

/myApp
  /functions
    /lib            // this is where the functions compile
    /node_modules   // functions' dependencies
    /src
      index.ts
      other.ts
    firebase.json   // configuration for this folder's contents
    package.json    // configuration for this folder's contents
  /next
    /node_modules   // Next.js app dependencies
    /.next          // compiled Next.js project
    /pages          // Next.js pages
    /public         // Next.js app public files
    /functions-lib
      app.js        // compiled Next function
    /functions-src
      app.ts        // source of Next function
    firebase.json   // configuration for this folder's contents
    package.json    // configuration for this folder's contents
    ...

Your /functions folder's firebase.json file would look similar to:

{
  "functions": {
    "source": ".",
    "predeploy": [
      "npm --prefix \"$RESOURCE_DIR\" run lint",
      "npm --prefix \"$RESOURCE_DIR\" run build"
    ]
  }
}

To get the above structure to behave properly, you need to tweak the configuration of the firebase.json and package.json files.

Add the following to the /function folder's package.json file:

"scripts": {
  "deploy": "firebase deploy --only functions:otherFunc1,functions:otherFunc2",
  ...
}

You could also export these functions as a single group for easy deployment, but the functions will be named like otherFunctions-otherFunc1, otherFunctions-otherFunc2, and so on:

// index.ts
import * as otherFunctions from './other.ts';
export { otherFunctions };
// package.json
"scripts": {
  "deploy": "firebase deploy --only functions:otherFunctions",
  ...
}

Your /next folder's firebase.json file would look similar to:

{
  "functions": {
    "source": ".",
    "predeploy": [
      "npm --prefix \"$RESOURCE_DIR\" run lint",
      "npm --prefix \"$RESOURCE_DIR\" run build"
    ]
  },
  "hosting": {
    "target": "myApp",
    "public": "public",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [{
      "source": "**",
      "function": "app"
    }]
  }
}

Add the following (or similar) to the /next folder's package.json file:

"scripts": {
  "build": "npm run build:next && npm run build:functions",
  "build:functions": "tsc",
  "build:next": "next build",
  "lint": "npm run lint:next && npm run lint:functions",
  "lint:functions": "eslint --ext .js,.ts ./functions-src",
  "lint:next": "...",
  "deploy": "firebase deploy --only hosting,functions:app"
}

With the above changes, drop into the appropriate directory and use npm run deploy whenever you would use firebase deploy.


Note: Don't forget that Cloud Functions consume the bodies of requests automatically, so make sure you account for this when using body parsers in Next projects and testing locally with next start.

Related