Requirements
- Understanding about Promises and async/await in Javascript.
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 newPet
-
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
andtypes.ts
inside thesrc
directory next to theindex.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 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:
...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.
Top comments (0)