DEV Community

Ozan H.
Ozan H.

Posted on

How To Write Maintainable and Clean Firebase Cloud Functions?

Recently for my IoT project i needed to develop and deploy endpoints with less effort and in less time. Because of all the speed that I want, I also wanted to make sure everything is safe, logged and auto scaled without worrying to manage many settings and code at all.

Aaaand, as you guess firebase cloud functions kicked in into the project. Tbh, I first started to develop with .NET but then development speed bothered me since I am the only developer for the whole IoT project.

The Problem With Functions

It's not mature as any specialized backend frameworks.

The topic will be about writing clean and maintainable middlewares, but I will try to cover how do i manage my overall functions project to keep it as much as close to mature backend frameworks.

What I am Trying To Achieve

The below onRequest method is not from https library, it's my custom build one which can handle middlewares as a list and executes them synchronously before running actual request handler.

// path: endpoints/admin/helloWorld.ts

export const helloWorld = onRequest<Request, Response>(
  (req, res) => {
    res.send({ error: ['Not implemented'] });
  },
  authRequiredMiddleware,
  anotherMiddleWare,
  andAnotherMiddleware
);
Enter fullscreen mode Exit fullscreen mode

Look at this code, it's very easy to read, it has typescript inferences, it has middleware array options.

Before going on how to achieve this output, first I want to show you my folder structure.

folder structure

I don't want to mess up with my endpoint files, I want them to be clean as much as possible and all the business logic will go to modules section, and this is also why I wanted to create a middleware handler so that my endpoint files stays as clean as possible.

So to create an endpoint just like above, we have to make use of https.onRequest and improve it.

Here is the enhanced onRequest function.

// path: utils/onRequest.ts

import { Response } from 'express';
import { https } from 'firebase-functions/v2';
import { HttpsOptions, Request } from 'firebase-functions/v2/https';

type GenericResponse<T> = {
  error?: string[];
  data?: T;
};

type t_request<T> = Omit<Request, 'body'> & { body: T }; // this line will pass our custom type to request.body of Request object.
type t_response<T> = Response<GenericResponse<T>>;
type t_fn<Req, Res> = (req: t_request<Req>, res: t_response<Res>) => void;
type t_middlewares = (req: Request, res: Response) => void | Promise<void>;

export const onRequest = <Req, Res>(fn: t_fn<Req, Res>, ...middlewares: t_middlewares[]) => {
  return https.onRequest(async (req, res) => {
    for (const middleware of middlewares) {
      await middleware(req, res); // We need to run each middleware synchronously.
    }
    safeFunctionExec(res, () => fn(req, res)); // See the below code piece.
  });
};
Enter fullscreen mode Exit fullscreen mode

Let's go over line by line what does this new onRequest function.

Basically, it's just a wrapper around https.onRequest function and it also make use of types to give you more hints about your Request and Response types.

The function takes only 2 parameters, one is the req, res handler and second one is the middlewares list.
This middlewares list allows us to easily pass as many as middleware we wish and execute them sequentially before the actual request handler get's executed.

In the example, there is also one more important function which is safeFunctionExec.

// path: utils/safeFunctionExec.ts

const safeFunctionExec = async (res: Response, fn: () => void | Promise<void>) => {
  const isSent = res.headersSent;
  if (!isSent) await fn();
};

export default safeFunctionExec;
Enter fullscreen mode Exit fullscreen mode

This utility function, wraps around every single middleware and custom onRequest method, and it checks if we returned already a response to the user within the execution of our custom middlewares. If any response is returned already, then we stop executing other middlewares and actual request handler function. This way we make sure our single function do not throw errors such as "headers already sent" etc.

Finally this is the main index file, we don't even touch it.

import admin from 'firebase-admin';
export * from '@endpoints/admin/hellWorld';
Enter fullscreen mode Exit fullscreen mode

Let's See The Whole Usage In Action

I will post it in one single code block, so you can see the picture clearly.

import { Response } from 'express';
import { https } from 'firebase-functions/v2';
import { HttpsOptions, Request } from 'firebase-functions/v2/https';

// Generic response, which is a best practice to keep organized your response through the all endpoints.
export type GenericResponse<T> = {
  error?: string[];
  data?: T;
};


// SAFE EXECUTOR
const safeFunctionExec = async (res: Response, fn: () => void | Promise<void>) => {
  const isSent = res.headersSent;
  if (!isSent) await fn();
};

// CUSTOM ON REQUEST FUNCTION
type t_request<T> = Omit<Request, 'body'> & { body: T };
type t_response<T> = Response<GenericResponse<T>>;
type t_fn<Req, Res> = (req: t_request<Req>, res: t_response<Res>) => void;
type t_middlewares = (req: Request, res: Response) => void | Promise<void>;

export const onRequest = <Req, Res>(fn: t_fn<Req, Res>, ...middlewares: t_middlewares[]) => {
  return https.onRequest(async (req, res) => {
    for (const middleware of middlewares) {
      await middleware(req, res);
    }
    safeFunctionExec(res, () => fn(req, res));
  });
};

// CUSTOM MIDDLEWARE
const authRequired = async (
  request: https.Request,
  response: Response<GenericResponse<undefined>>
): Promise<void> => {
  // Each custom middleware should be wrapped with safe function wrapper, because we need to check if response is sent and kill the process immediately if it's sent already.
  await safeFunctionExec(response, async () => {
    const auth = request.headers.authorization;

    if (auth) {
      try {
        const token = auth.split(' ')[1];
        await admin.auth().verifyIdToken(token);
      } catch (error) {
        logger.error(`onRequestAuthProtection token parse error ${error}`);
        response.status(401).send({ error: ['Token validation errors'] });
      }
    } else {
      response.status(401).send({ error: ['Token not found'] });
    }
  });
};

// A SIMPLE ENDPOINT
export const helloWorld = onRequest<MyRequestType, MyResponseType>((req, res) => {
  res.send({ error: ['Not implemented'] });
}, authRequired);
Enter fullscreen mode Exit fullscreen mode

End

I believe firebase functions keeps improved for sure but I hope it can get more mature soon.

Top comments (0)