Title: An Introduction to Serverless Functions Using Node.js .
Introduction:
Serverless computing is a revolutionary cloud computing model that allows developers to focus solely on writing code without the need to manage servers. In this blog, we will explore the world of serverless functions using Node.js, one of the most popular programming languages for serverless development. We'll discover how serverless functions work, their advantages, and how to build and deploy them using Node.js and the Serverless Framework.
What is Serverless Computing?
Serverless computing, also known as Function-as-a-Service (FaaS), is a cloud computing paradigm where developers deploy individual functions as event-driven services. These functions are executed in stateless containers that are triggered by specific events, such as HTTP requests, database changes, or file uploads. Unlike traditional server-based architectures, serverless functions automatically scale based on demand, leading to cost optimization and improved performance.
Key Characteristics of Serverless Functions:
Here's a table summarizing the differences between traditional server architecture and serverless functions:
Keep in mind that both traditional server architecture and serverless functions have their strengths and are suitable for different use cases. Traditional server architecture provides more control over the infrastructure but requires more operational effort, while serverless functions abstract away the infrastructure management, allowing developers to focus on building application logic without worrying about scaling and server maintenance. The choice between the two depends on the specific requirements and constraints of the application.
| Traditional Server Architecture | Serverless |
Server Management | Developers manage servers and infrastructure. | No server management; handled by cloud provider. |
Scaling | Manual scaling requires capacity forecasting. | Automatic scaling based on incoming events. |
Continuous Availability | Requires redundancy and failover mechanisms. | High availability managed by cloud providers. |
Cost Model | Fixed cost for provisioned servers. | Pay-as-you-go billing based on actual usage. |
Deployment Complexity | Deployment may involve manual steps | Simplified and faster deployment. |
Execution Model | Servers run continuously, consuming resources. | Short-lived functions that execute per event. |
Getting Started with Node.js and the Serverless Framework:
I'll walk you through the step-by-step procedure to set up Firebase Cloud Functions, including setting up Firebase, initializing the project with Firestore, Functions, and Emulators, and writing and testing a simple function.
Step 1: Install Firebase CLI
If you haven't installed the Firebase CLI, open your terminal and run the following command to install it globally on your system:
npm install -g firebase-tools
Step 2: Log in to Firebase
After installing the Firebase CLI, log in to your Firebase account using the following command:
firebase login
A browser window will open, and you'll be asked to log in with your Google account. After successful login, return to the terminal.
Step 3: Initialize Firebase Project
Now, navigate to the directory where you want to create your Firebase project, or create a new directory, and run the following command to initialize Firebase:
firebase init
You will be presented with a series of prompts. Use the arrow keys to select the following options:
- Use the arrow keys to navigate: Firestore
, Functions
, and Emulators
.
- Use an existing project: Select the Firebase project you want to use (or create a new one).
Step 4: Configure Firestore and Functions
Next, configure Firestore and Functions by selecting the appropriate options based on your preferences and project requirements in our case select Firestore, Functions and Emulators. On the following prompts choose all defaults. For Firestore, you might choose to set security rules (default or custom) during the initialization.
We will be using typescript as the language and npm as a dependency management tool in our example. Choose typescript and npm in the configurations.
Step 5: Setting up the Firestore Emulator and the Functions Emulator for testing your Firebase Cloud Functions locally. Here's how you can proceed:
Use the arrow keys to navigate and select the Firebase emulators you want to set up for local testing. In your case, choose the Firestore Emulator and the Functions Emulator. Press the spacebar to select them. The selected emulators will have a checkmark (✓) next to them.
Once you have selected the emulators you want to use, press Enter to confirm your choices and then select default ports for the emulators and finish up with the download.
Step 6: Install firebase-backend
Module
In the functions
directory, install the firebase-backend
module using npm:
npm install firebase-backend
Let's start by going through a high-level overview of how the backend will be set up. This overview will go over the types of functions we use as well as the actual code structure.
Types of Functions
The backend is built around the strengths that Firebase poses in its serverless cloud functions setup. Focussing on those strengths we can break the system into two types of functions (could also be called a micro-service if you choose to). Reactive and RESTul
Reactive: This is a function that will run in reaction to data or state updating on the backend. An example of this will be when a file is uploaded to cloud storage or most commonly when a document/entry in the database has been updated.
RESTful: This is the function that will run when the user makes an HTTP request to the URI the function is assigned to. Nothing special about these. Just 1 note that's very important. This will not be used for single CRUD operations like adding a user, deleting a user, or updating a user. And it's built with that in mind. This means you won't be able to define a single API endpoint for the user that behaves differently based on the HTTP verb used. This is by design and won't be changed. All CRUD should be performed directly on your Firebase DB of choice. That's how this is supposed to be used.
Code structure
We have an enforced code structure that will help with the organization of the backend as well as the overall maintenance as it grows. There are 3 major things to go over.
Each function will be in its dedicated file: This is to get rid of the "natural" tendency, when starting with Firebase cloud functions, to keep adding functions into the same index file forcing it to grow bigger as your backend requirements grow. The file name will be the exact name of the endpoint to keep things easy to manage. This is not a requirement but I've found it to be quite helpful.
Functions will be placed in a folder titled either restful or reactive
The backend will be split into different resource groups to ensure a structured backend in production
Organize your Firebase functions folder into api domain folders (groups) and function types (reactive, restful).
src
{group_name_folder}
reactive
- onSomeTrigger.function.ts
- onSomeOtherTrigger.function.ts
restful
- someEndpointName.endpoint.ts
- someOtherEndpointName.endpoint.ts
- index.ts
package.json
Configuration
Then you can open the index.ts file in your source folder and update it to
import { FunctionParser } from 'firebase-backend';
exports = new FunctionParser({ rootPath: __dirname, exports, verbose: true })
.exports
;
These are the two magical lines of code that allow us to dynamically add and export functions as the backend grows without ever changing the index file. And that's also all we need to set it up. Now we can start creating functions.
Restful Functions (Endpoints)
Create
Let's say we wanted to make an endpoint where a client application could add a payment method for a user.
The API would be called users
The function would be called addPaymentMethod
The file would be called src/users/restful/addPaymentMethod.endpoint.ts
The endpoint name will be exactly the name of your file
The endpoint.ts file extension identifies the function as an HTTP endpoint
// src/users/restful/addPaymentMethod.endpoint.ts
import { Request, Response } from 'express';
import { Post } from 'firebase-backend'; // Get, Post, Put, Update, Delete available
// Use the Post-class which is extended from the Endpoint class.
export default new Post((request: Request, response: Response) => {
// Read the values out of the body
const cardNumber = request.body['card_number'];
const cardHolder = request.body['card_holder'];
// Do your thing with the values
let paymentToken = ${cardNumber}_${cardHolder};
// Send your response. 201 to indicate the creation of a new resource
return response.status(201).send({
token: paymentToken,
});
});
Testing
To test this out we'll run the following command in the functions folder.
npm run serve
This will build the TypeScript code and then serve the functions locally through the emulator. If this is successful you should see the following in the console. You should see the functions API has deployed (locally) a function at the following url
http://localhost:5001/boxtout-fireship/us-central1/users-api
All the endpoints in the users resource group will be deployed under the /user-api function. This means that we can make a post request to the endpoint with the expected data and check if we get back a result. I'm going to use PostMan to test this out. So we'll put in the above URL and add /add a payment method at the end of it. Select post as the HTTP request type and then pass in a body.
Reactive Functions
Create
Let's say we wanted to make a function that would run when the Firestore db had a user record updated.
The API would be called the users
The function would be called onUserCreated
The file would be called src/users/reactive/onUserCreated.function.ts
The endpoint name will be exactly the name of your file
The function.ts file extension identifies the function as reactive
// src/users/reactive/onUserCreated.function.ts
import * as functions from 'firebase-functions';
export default functions.firestore
.document('users/{userId}')
.onCreate((userSnapshot, context) => {
const data =
userSnapshot.data
();
console.log(`User Created | send an email to ${
data.email
}`);
});
Testing
Run npm run build in the functions folder. Then run firebase emulators:start.
You should now have a function deployed at users-onUserCreated as well as at users-api. All the api endpoints go under the one api function, but the reactive functions are added as their own functions. Let's test this out.
At the bottom of your logs you'll see a link to firestore http://localhost:4000/firestore . Open that in your browser. You'll see an empty page. Click on start collection, make the collection id users . Add a field called email and put the value dane@filledstacks.com and save the document. When this is saved you should see the logs printing out the following message
i functions: Beginning execution of "users-onUserCreated"
> User id created TybqxAwnC4X5DWLgtXOp
> {"severity":"WARNING","message":"Function returned undefined, expected Promise or value"}
i functions: Finished "users-onUserCreated" in ~1s
> User Created | send an email to
dane@filledstacks.com
And that's it! You've created a reactive function as well as a http endpoint. Going further when you want to expand you backend you simply create a new file in the dedicated folder depending on the function type and it'll be added automatically .
We can finally deploy the functions to the cloud run :
npm run deploy