The way to split the file of Cloud Functions Triggers

The way to split the file of Cloud Functions Triggers

September 25, 2021

This article is a translation of a Japanese article I posted earlier.

Original article


After init of the Firebase CLI, the files related to Cloud Functions will be as follows (In the case of TypeScript).

└ functions/
(ohter files)
   ├ lib/
   └ src/
     └ index.ts

All you have to do is write the trigger function in index.ts

However, if you create a lot of triggers, you will definitely be tempted to split the files, right?

So, I’m going to post an example of how I split it up.

TL;DR #

I list each file first, then explain later.

Structure and files #

(All files except for those under the src directory are omitted.)

└ functions/
   └ src/
     ├ firebase.ts
     ├ index.ts
     └ triggers/
        ├ sampleCreate.ts
        ├ sampleDelete.ts
        ├ sampleUpdate.ts
        └ sampleHttps.ts

firebase.ts #

import * as functions from 'firebase-functions';

export type Snapshot = functions.firestore.QueryDocumentSnapshot;
export type Change = functions.Change<functions.firestore.QueryDocumentSnapshot>;
export type Context = functions.EventContext;
export type Request = functions.https.Request;
export type Response = functions.Response;

type SnapshotHandler = { trigger: (snapshot: Snapshot, context: Context) => Promise<unknown> };
type ChangeHandler = { trigger: (change: Change, context: Context) => Promise<unknown> };
type RequestHandler = { trigger: (req: Request, resp: Response) => Promise<unknown> };

const getHandler = async (handlerFileName: string) => {
  const handlerFilePath = `./triggers/${handlerFileName}`;
  return await import(handlerFilePath);
};

const db = functions.region('asia-northeast1').firestore; // Change the region as needed.
const https = functions.region('asia-northeast1').https; // Change the region as needed.

/**
 * @param documentPath - Path of the Firestore document where the trigger target.
 * @param handlerFileName - File name under "./triggers" directory (excluding extensions).
 */
export const onCreate = (documentPath: string, handlerFileName: string) => {
  return db.document(documentPath).onCreate(async (snapshot, context) => {
    const handler: SnapshotHandler = await getHandler(handlerFileName);
    return handler.trigger(snapshot, context);
  });
};

/**
 * @param documentPath - Path of the Firestore document where the trigger target.
 * @param handlerFileName - File name under "./triggers" directory (excluding extensions).
 */
export const onDelete = (documentPath: string, handlerFileName: string) => {
  return db.document(documentPath).onDelete(async (snapshot, context) => {
    const handler: SnapshotHandler = await getHandler(handlerFileName);
    return handler.trigger(snapshot, context);
  });
};

/**
 * @param documentPath - Path of the Firestore document where the trigger target.
 * @param handlerFileName - File name under "./triggers" directory (excluding extensions).
 */
export const onUpdate = (documentPath: string, handlerFileName: string) => {
  return db.document(documentPath).onUpdate(async (change, context) => {
    const handler: ChangeHandler = await getHandler(handlerFileName);
    return handler.trigger(change, context);
  });
};

/**
 * @param handlerFileName - File name under "./triggers" directory (excluding extensions).
 */
export const onHttps = (handlerFileName: string) => {
  return https.onRequest(async (req, resp) => {
    const handler: RequestHandler = await getHandler(handlerFileName);
    return handler.trigger(req, resp);
  });
};

index.ts #

import { onCreate, onDelete, onUpdate, onHttps } from './firebase';

export const sampleCreate = onCreate('/collection/{id}', 'sampleCreate');
export const sampleDelete = onDelete('/collection/{id}', 'sampleDelete');
export const sampleUpdate = onUpdate('/collection/{id}', 'sampleUpdate');
export const sampleHttps = onHttps('sampleHttps');

sampleCreate.ts #

import type { Snapshot, Context } from '../firebase';

export const trigger = async (snapshot: Snapshot, context: Context) => {
  // Do something ...
};

sampleDelete.ts #

import type { Snapshot, Context } from '../firebase';

export const trigger = async (snapshot: Snapshot, context: Context) => {
  // Do something ...
};

sampleUpdate.ts #

import type { Change, Context } from '../firebase';

export const trigger = async (change: Change, context: Context) => {
  // Do something ...
};

sampleHttps.ts #

import type { Request, Response } from '../firebase';

export const trigger = async (request: Request, response: Response) => {
  // Do something ...
};

Explanation #

The process flow is as follows.

Each function under triggers/ is passed through firebase.ts to index.ts. To explain from the other side, the functions in firebase.ts are executed from index.ts, and the functions in triggers/ are called inside of it.

As you can see from the amount of code, the key is firebase.ts. Thanks to this, each trigger function can be thinned.

I also wrote some comments (JSDoc) in firebase.ts, so please refer to it. (sorry for poor comment.)

To add triggers, simply create a file under triggers/ and add a line to index.ts.

Please note that if you want to change the directory name triggers/, you should also modify the directory name in getHandler() of firebase.ts.

Point: Import only the called trigger file #

In addition to splitting the files, one other thing I took care of was to use dynamic import to import only the trigger files that were called.

The getHandler() in firebase.ts is it.

If you do this, for example, when sampleCreate is running, other sampleDelete, sampleUpdate, and sampleHttps trigger files will not be imported.

This will save you a small amount of startup time and computer resources (and thus your money) compared to importing everything.

Thank you for reading. #

I hope this article was helpful!