Firebase Functions Express Typescript Project Guide Part 2

RMAG news

In this part we will create a REST API to perform CRUD operations in a Pets resource. We will be able to use this API to interact with our Pets Firestore Collection like dogs or cats.

First it is important to understand the Best Practices of REST API Design, please read this article.

Note: Remember to build the Functions project every time you make a new change with npm run buil this will recreate the lib directory.

0. Design First Path

Now, that you have read about the best practices let’s design our paths. Let’s say that our default URL will be http://127.0.0.1:5001/my-project/us-central1/api

First we want to create Pets, so our first path should be /pets . This endpoint will create a brand new pet and it should return the new resource created along with its database ID, so we can identify it later.
The HTTP method we use to create a new resource is POST so we will create a new function that accepts a POST request and creates a new Pet

It will look something like this:

app.post(/pets, (req: Request, res: Response) => // create a new pet );

1. Create Pet Model

Now that we created our first POST path, we now we want to create Pets, but how would a pet look like, what properties should it have? Let’s use a simple example, let’s say a pet has these properties:

Pet
id
category
name
tags

Now all of this properties need a Firestore Type like boolean, string or number. Let’s define them:

Pet
id: string;
category: string;
name: string;
tags: Array of strings;

Now we have the properties for our Pet model, we need to use the power of Typescript to define this model in our code.

First let’s define what Typescript uses interfaces and types for [GPT complete this part]

Now that we now how interfaces and types work, let’s create our Pet interface:

interface Pet {
id: string;
category: string;
name: string;
tags: string[]; // Array of strings
}

Now, probably we want to have explicit categories, so we don’t pets without a valid category, we can do that with a Type:

type PetCategory = dog | cat | bird

And the we update our Pet interface:

interface Pet {
id: string;
category: PetCategory;
name: string;
tags: string[]; // Array of strings
}

This way we will make sure only strings of those values can be passed to a Pet interface and if not, the TS compiler will throw an error.

Now that we have our type and interfaces, let’s add them to our code. The good practice is to create separate files for them an import them as necessary. Let’s create an interfaces.ts and types.ts inside the src directory next to the index.ts file.

The content of the types file should be:

export type PetCategory = dog | cat | bird;

And the interfaces file should look like this:

// We create a relative import to use the PetCategory type in our interface
import {PetCategory} from ./types;

export interface Pet {
id: string;
category: PetCategory;
name: string;
tags: string[]; // Array of strings
}

Great! Now we have a new Pet model.

2. Initialize Firestore

First let’s add the Firestore emulator, otherwise all calls will be added to your Firebase Project’s Firestore Database you created un part 1 and we want to test in our local environment first.

Let’s modify the serve script in package.json file to add Firestore emulators:

serve: npm run build && firebase emulators:start –only=functions,firestore

Let’s run npm serve and you should see something like this:

i firestore: Firestore Emulator logging to firestoredebug.log
firestore: Firestore Emulator UI websocket is running on 9150.

┌───────────┬────────────────┬─────────────────────────────────┐
Emulator Host:Port View in Emulator UI
├───────────┼────────────────┼─────────────────────────────────┤
Functions 127.0.0.1:5001 http://127.0.0.1:4000/functions │
├───────────┼────────────────┼─────────────────────────────────┤
Firestore 127.0.0.1:8080 http://127.0.0.1:4000/firestore │
└───────────┴────────────────┴─────────────────────────────────┘

Now let’s initialize Firestore in the main.ts file:

other code

// Import Firebase admin to use Firestore
import * as admin from firebase-admin;

// Import the Pet interface
import {Pet} from ./interfaces;

// Initialize Firebase Admin SDK
admin.initializeApp();

// Firestore database reference
const db = admin.firestore();

Let’s also use express.json in our express app to parse requests:

other code

const app = express();

// Middleware to parse JSON requests
app.use(express.json());

Now build the project to update the emulators server

3. Create new Pet

Let’s write a function to create the new Pet, but first read this:

// Define a POST route for creating new pets
app.post(/pets, async (req: Request, res: Response) => {
// Extract the new pet data from the request body
const newPet: CreatePetBody = req.body;

try {
// Add the new pet to the “pets” collection in Firestore
const docRef = await db.collection(pets).add(newPet);

// Create a pet document with the generated ID and pet data
const petDocument: PetDocument = {id: docRef.id, newPet};

// Send the created pet document as the response with status 201
res.status(201).send(petDocument);
} catch (error) {
// Send an error response with status 500 if something goes wrong
res.status(500).send(Error creating new pet: + error);
}
});

Now build the project to update the emulators server

Open postman, create a new POST request to the default URL and the path /pets

Select the Body, then select Raw and JSON

Now add the following:

{
category: dog,
name: Buddy,
tags: [friendly, playful]
}

If it worked, the Postman response should look like this:

{
id: Jth82AgC7CIMxxXQKPEF,
category: dog,
name: Buddy,
tags: [
friendly,
playful
]
}

Now we can use that id to retrieve that specific pet in the future and if you go to http://localhost:4000/firestore you should see the new data in the emulator

4. What if some parameters are missing?

So what happens if someone tries to create a Pet without a name? We will create a Document that looks like this:

{
id: Jth82AgC7CIMxxXQKPEF,
category: dog,
tags: [
friendly,
playful
]
}

This is not correct and can lead to data corruption, we need to make sure we are persisting the data following our interfaces and type models. So let’s add a check:

if (!newPet.name) {
res.status(400).send(Bad request: missing name);
}

Also we should check that the category is passed as an argument:

if (!newPet.category) {
res.status(400).send(Bad request: missing category);
}

Try to create a pet without category or name, you should receive a Bad request: missing name response

What about the tags? The tags is an array of strings, we can either set it as an empty array or as an optional property. Let’s add it as optional, modify the interface to add a ? that tells typescript this is an optional property:

import {PetCategory} from ./types;

export interface Pet {
category: PetCategory;
name: string;
tags?: string[]; // Optional ? Array of strings
}

Cool. Now we can create Pets.

5. GET Pets and filter by category and tag

After creating the Pets, it’s time to read those Pets:

Read this first to learn how to get data from Firestore

Now let’s get all our pets:

app.get(/pets, async (req: Request, res: Response) => {
try {
// Reference to the pets collection
// CollectionReference: https://firebase.google.com/docs/reference/node/firebase.firestore.CollectionReference
const petsRef: admin.firestore.Query = db.collection(pets);

// Fetch pets from Firestore
// This declaration returns a Firestore QuerySnapshot:
// https://firebase.google.com/docs/reference/node/firebase.firestore.QuerySnapshot
const snapshot = await petsRef.get();

// Why do we need to iterate over docs? We need to iterate
// over docs to convert each document into a usable object
const pets: PetResponse[] = snapshot.docs
.map((docSnapshot: admin.firestore.QueryDocumentSnapshot) => {
// What is “as”? “as” is a TypeScript type assertion,
// telling the compiler that docSnapshot.data() is of type Pet
const pet: Pet = docSnapshot.data() as Pet;

// Return the data along with the Document ID
// Combining the document ID with the pet data into one object
return {
id: docSnapshot.id,
data: pet,
};
});

// Send the processed and filtered pets as the response
res.status(200).send(pets);
} catch (error) {
// Send an error response with status 500 if something goes wrong
res.status(500).send(Error reading pets: + error);
}
});

So what if we want to search for pets that have only the dog category. In REST API design, we can do that with query parameters added to our path like so:

api/pets?category=dog

We can other queries as well:

api/pets?category=dog&tag=intelligent

This way we can tell Firestore to filter the results like this:

db.collection(pets).where(category, ==, dog);

So the final code should look like this:

app.get(“/pets”, async (req: Request, res: Response) => {
try {
// Get query parameters
const {category, tag} = req.query;

// Reference to the pets collection
// CollectionReference: https://firebase.google.com/docs/reference/node/firebase.firestore.CollectionReference)
let petsRef: admin.firestore.Query = db.collection(“pets”);

// Apply category filter if provided
if (category) {
petsRef = petsRef.where(“category”, “==”, category);
}

// Apply category filter if provided
if (tag) {
petsRef = petsRef.where(“tag”, “==”, tag);
}

// Fetch pets from Firestore
// This declaration returns a Firestore QuerySnapshot:
// https://firebase.google.com/docs/reference/node/firebase.firestore.QuerySnapshot
const snapshot = await petsRef.get();

// What’s going on? Why do we need to iterate over docs?
const pets: PetResponse[] = snapshot.docs
.map((docSnapshot: admin.firestore.QueryDocumentSnapshot) => {
// What’s going on here? What is “as”?
const pet: Pet = docSnapshot.data() as Pet;

// Return the data along with the Document ID
return {
id: docSnapshot.id,
data: pet,
};
});

// Send the processed and filtered pets as the response
res.status(200).send(pets);
} catch (error) {
// Send an error response with status 500 if something goes wrong
res.status(500).send(“Error reading pets: ” + error);
}
});

Hell yeah! Now we have created and filtered virtual animals.

Leave a Reply

Your email address will not be published. Required fields are marked *