Firebase Cloud Functions uses the Firebase framework so you can have one backend for your whole project. This has the benefit of being serverless, so you do not have to manage the running of your API yourself.
-
Create a new project (or if you have already done this, open it by clicking on it).
-
Wait for project to be created.
-
On the left hand pane, click on
Build->Functions. -
As
Functionsis not included in theSpark(default) plan, you will have to upgrade to theBlazeplan.[!WARNING] The Blaze plan will cost you if your quota runs out, so be careful with your usage! You are very unlikely to run out of your quota within the timeframe of a hackathon!
-
Create a Cloud Billing Account.
-
Follow the instructions in the new window.
-
Click
Get Startedonce you have changed to theBlazeplan. -
As prompted, run
npm install -g firebase-tools -
Run
firebase login. -
In your project root, run
firebase init. -
At the very least, select
Functions. -
Select
Use an existing projectand select your project. -
Choose your language for Cloud Functions. For this guide, we will be using TypeScript, but links will be provided for Python alternatives.
-
Enable ESLint (recommended, not required).
-
Install dependencies as prompted.
Firebase will have created a whole directory structure like the one below:
example-project/
├─ functions/
│ ├─ src/
│ │ └─ index.ts
│ ├─ package.json
│ └─ tsconfig.json
├─ firebase.json
└─ .firebasercNow that you have set up your Cloud Functions directory, we will get to writing some example functions themselves.
Add the following to the end of src/index.ts
// The Firebase Admin SDK to access Firestore.
import {initializeApp} from "firebase-admin/app";
import {getFirestore} from "firebase-admin/firestore";
initializeApp();This will setup your cloud functions in Admin mode, allowing your backend full access to Firebase's framework (Firestore, etc).
All the functions you create need to be exposed as
export const [fun_name] = [fun_def]in index.ts.
You can go about this in one of two ways:
- If you don't have many functions, you can write all your functions directly inside
index.ts. - If you have many, you will want to split them into several files. I would recommend this approach.
To demonstrate a simple GET request, we will create a new file basic.ts in the src folder.
import * as functions from "firebase-functions";
export const helloWorld = functions.https.onRequest((req, res) => {
res.send("Hello ICHack!");
});This is a simple HTTP GET function that returns "Hello ICHack!" to the caller.
Since this is not in index.ts, we now need to expose it there.
We add the following to index.ts
import * as basic from "./basic";
export const helloWorld = basic.helloWorld;We use a qualified import so we can reuse the same name for the function.
Now that we have written our first function, we can deploy it!
Deploying your functions is just publishing your code to Firebase's servers so your clients can call them.
To deploy, in your project directory, call
firebase deployThere is a high chance that there will be deployment failures due to ESLint errors. Fix those errors and re-run the command.
Important
While setting this up myself, I got the following error
Error: Request to https://serviceusage.googleapis.com/v1/projects/X/services/run.googleapis.com:enable had HTTP Error: 429, Quota exceeded for quota metric 'Mutate requests' and limit 'Mutate requests per minute' of service 'serviceusage.googleapis.com' for consumer 'project_number:X'.
This is Google rate-limiting you when you are enabling the various APIs. Wait for a minute or two and retry.
Note
The deployment may take a short while, be patient!
Once the process is complete, you will be provided with the URL where your function can be accessed. For me, this was
Function URL (helloWorld(us-central1)): https://us-central1-sample-project-44e1c.cloudfunctions.net/helloWorldImportant
Once the setup is complete, you may be asked
How many days do you want to keep container images before they're deleted?
To avoid surprise bills, press 1 and Enter. This cleans up the containers for tasks older than a day.
As you may have noticed, where do the HTTP methods and status codes come into this?
Each function you create represents a resource, an endpoint. The function itself will handle what HTTP method is used.
Below is how you would go about it
import * as functions from "firebase-functions";
export const helloWorld = functions.https.onRequest(async (req, res) => {
switch (req.method) {
case "GET":
res.status(200).send("Hello ICHack!");
break;
case "POST": {
// Increment counter
res.status(200).json({ newCount: ... });
break;
}
case "DELETE":
// Reset counter
res.status(204);
break;
default:
res.status(404).send("Method Not Allowed");
}
});Now on to a specific resource, the canonical RESTful way of doing this is with /posts/{postId}. We will show why this may not be the best with Firestore, and that packing the postId into the request body may be better.
In order to properly parse the resource, we must inspect the path.
const id = req.path.split("/")[1]; // /posts/{id}The above code gets the request path, and splits on the /. This will extract the id value so it can be used by our backend.
View an example canonical Firebase backend.
For our hacky, simpler method, we just pack the request body with all the data we need. Requests will all go to the /posts endpoint but with different arguments.
const { id, text } = req.body;The above unpacks the request body into the id and text fields.
Warning
This typically breaks caching, so the canonical method is preferred...
View an example hacky Firebase backend.
In general, you send a simple string response with the
res.send("Message");and you send a JSON response with
res.json(data)Firebase comes with a special type of API - a callable function.
There are many benefits:
- Auth tokens are automatically included and handled
- No need for custom parsing
- No need to explicitly handle HTTP error codes
- No need for CORS handling
- On the frontend, it is a simple function call
To create a callable function, you just need to use the following structure
import { onCall, CallableRequest } from "firebase-functions/v2/https";
export const fun = onCall(
async (request: CallableRequest<{ arg1?: number, arg2?: string,}>) => {
const { arg1, arg2 } = request.data;
// your function
const response = {
res1: val1,
res2: val2,
ok: true,
};
return response;
}
);An example of this can be found by clicking here
If you were to deploy and run your API and call these functions from a browser front-end, you will end up with CORS errors.
This step can be avoided if you choose the Firebase callable functions method of calling your API (more on this later...)
To suppress CORS errors, simply add {cors: true} as shown below, to all your functions.
export const helloWorld = functions.https.onRequest(
{cors: true},
async (req, res) => {
// your function logic
}
);Warning
Do not use this in production. This allows requests from any source. Instead you should use {cors: [your_domain_one.com, ...]}
However this is perfectly fine for hackathons!
Tip
Don't forget to redeploy your functions using firebase deploy after you edit them!
Now that we know how to create the backend, we now need to know how to use it!
As alluded to already, Firebase has 2 main ways to do this:
- The canonical HTTP request
- A callable function
This has been covered by Making Requests. But I want to point out how the frontend requests differ based on the approach used.
With the RESTful approach, we need to add the post ID to the API URL.
export async function updatePost(id: string, text: string) {
const url = `${API}/${id}`;
const res = await fetch(url, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text }),
});
return res.json();
}View a full example frontend for a RESTful Firebase backend.
Whereas with the hacky approach, we pack the ID into the request body.
export async function updatePost(id: string, text: string) {
const res = await fetch(API, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id, text }),
});
return res.json();
}If you have not done so yet, run the following in the root of the frontend
npm install firebaseNow add your web app.
-
On Firebase Console, open your project
-
Press the
+ Add appbutton -
Select Web
-
Copy the code shown, it should look something like
// Import the functions you need from the SDKs you need import { initializeApp } from "firebase/app"; // TODO: Add SDKs for Firebase products that you want to use // https://firebase.google.com/docs/web/setup#available-libraries // Your web app's Firebase configuration const firebaseConfig = { apiKey: "__", authDomain: "__", projectId: "__", storageBucket: "__", messagingSenderId: "__", appId: "__" }; // Initialize Firebase const app = initializeApp(firebaseConfig);
-
Copy this into a new file called
firebase.ts(or.js) infrontend/src -
Add the necessary code to import cloud functions, as shown below
// Import the functions you need from the SDKs you need import { initializeApp } from "firebase/app"; import { getFunctions } from "firebase/functions"; // Your web app's Firebase configuration const firebaseConfig = { ... }; // Initialize Firebase export const app = initializeApp(firebaseConfig); export const functions = getFunctions(app); // anything else you will need
-
Now add the following to the top of your API file
import { httpsCallable } from "firebase/functions"; import { functions } from "./firebase"; const yourFuncName = httpsCallable(functions, "yourFuncName");
-
Call them like you would any other function!
Below is an example of a function from the sample project
import { httpsCallable } from "firebase/functions";
import { functions } from "./firebase";
// Not necessary but good for consistency
type PostsRequest = {
action: "create" | "get" | "update" | "delete"
id?: string
text?: string
}
type PostsResponse =
| { id: string } // create
| { ok: true } // update/delete
| { id: string; text: string } // get single
| { id: string; text: string }[] // get all
const postsCallable = httpsCallable<PostsRequest, PostsResponse>(functions, "postCallable");
export async function getPosts(id?: string) {
try {
const res = await postsCallable({ action: "get", id });
return res.data;
} catch (err: any) {
throw new Error(err.message || "Network Error");
}
}