Firebase Functions Express Typescript Project Guide Part 2
Tags: webdev, typescript, express, firebase
Published: 2024-05-14 14:45:53

## Requirements
- Understanding about Promises and [async/await in Javascript](https://www.freecodecamp.org/news/javascript-async-await/).

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](https://stackoverflow.blog/2020/03/02/best-practices-for-rest-api-design/).

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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST)** so we will create a new function that accepts a `POST` request and creates a new `Pet`
- It will look something like this:
    
    ```tsx
    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:
    
    ```markdown
    Pet
    	- id
      - category
      - name
      - tags
    ```
    
- Now all of this properties need a [Firestore Type](https://firebase.google.com/docs/firestore/data-model#:~:text=Note%3A%20Cloud%20Firestore%20supports%20a,structure%20data%20within%20a%20document.&text=You%20may%20notice%20that%20documents,In%20fact%2C%20they%20basically%20are.) like boolean, string or number. Let’s define them:
    
    ```markdown
    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:
    
    ```tsx
    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:
    
    ```tsx
    type PetCategory = 'dog' | 'cat' | 'bird'
    ```
    
- And the we update our **Pet** interface:
    
    ```tsx
    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:
    
    ```tsx
    export type PetCategory = "dog" | "cat" | "bird";
    ```
    
- And the interfaces file should look like this:
    
    ```tsx
    // 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:
    
    ```tsx
    "serve": "npm run build && firebase emulators:start --only=functions,firestore"
    ```
    
- Let’s run `npm serve` and you should see something like this:
    
    ```tsx
    i  firestore: Firestore Emulator logging to firestore-debug.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:
    
    ```tsx
    ...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:
    
    ```tsx
    
    ...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](https://firebase.google.com/docs/firestore/manage-data/add-data):
    
    ```tsx
    // 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:
    
    ```tsx
    {
        "category": "dog",
        "name": "Buddy",
        "tags": ["friendly", "playful"]
    }
    ```
    
- If it worked, the Postman response should look like this:
    
    ```tsx
    {
        "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:
    
    ```tsx
    {
        "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:
    
    ```tsx
    if (!newPet.name) {
        res.status(400).send("Bad request: missing name");
      }
    ```
    
- Also we should check that the `category` is passed as an argument:
    
    ```tsx
    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:
    
    ```tsx
    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](https://firebase.google.com/docs/firestore/query-data/get-data) to learn how to get data from Firestore
- Now let’s get all our pets:
    
    ```tsx
    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:
    
    ```tsx
    ...api/pets?category=dog
    ```
    
- We can other queries as well:
    
    ```tsx
    ...api/pets?category=dog&tag=intelligent
    ```
    
- This way we can tell Firestore to filter the results like this:
    
    ```tsx
    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.