A Node.js REST API example for Firebase, built with TypeScript, Express, Firebase Authentication, Firebase Admin SDK, and Firestore. It also handles Event Triggers (2nd gen) so all your code is organized. This project fits well to be used as a template for the creation of new servers.
The main aspects of this sample are:
-
An API HTTP Trigger:
- A well-organized API under the
apifolder - Access Control: Reject user access by simply choosing what user roles can access a specific path or easily check the claims with a custom
requestobject in the Request Handler - Reject a request anywhere by throwing
new HttpResponseError(status, codeString, message)
- A well-organized API under the
-
Events Triggers (2nd gen):
- A well-organized Events Triggers under the
event-triggersfolder
- A well-organized Events Triggers under the
-
Shared components between API and Event Triggers are under the
corefolder
This example is a good start if you are building a Firebase Cloud Functions project.
Every time a user or product is created, or a product is updated,
a new record is created in
the db-changes Firestore Collection that only admins can access,
the code for these triggers is inside the event-triggers folder.
The triggers are onUserCreated, onProductCreated, and onProductUpdated.
There are three roles: storeOwner, buyer and admin.
Anyone can create an account, but an adminKey is required to create
a user with admin role.
Store Owners:
- ✅ Create products
- ✅ List public products data
- ✅ Get full data of his own product
- ❌ Get full data of other store owners' product
- ❌ List records of changes made inside the DB, like "Product Blouse has been updated"
Buyers:
- ✅ List public products data
- ❌ Create products
- ❌ Get full data of a product
- ❌ List records of changes made inside the DB, like "Product Blouse has been updated"
Admins:
- ✅ Create products
- ✅ List public products data
- ✅ Get full data of ANY product
- ✅ List records of changes made inside the DB, like "Product Blouse has been updated"
In the Firebase Console:
-
Go to Build > Authentication > Get Started > Sign-in method > Email/Password and enable Email/Password and save it.
-
Also go to Build > Firestore Database > Create database. You can choose the option
Start in test mode
Go to the functions folder and run npm install
to install the dependencies. After that,
go back to the root folder (cd ..) and run:
npm install -g firebase-toolsto install the Firebase CLIfirebase use --addand select your Firebase project, add any alias you prefer- And finally, run
firebase deploy
Firebase Authentication is used to verify
if the client is authenticated on Firebase Authentication,
to do so, the client side should inform the Authorization header:
The client's ID Token on Firebase Authentication in the format Bearer <idToken>,
it can be obtained on the client side after the authentication is performed with the
Firebase Authentication library for the client side.
It can be generated by the client side only.
Follow the previous instructions on Use Postman to test it and pass
it as Authorization header value in the format Bearer <idToken>
final idToken = await FirebaseAuth.instance.currentUser!.getIdToken();
// use idToken as `Authorization` header value in the format "Bearer <idToken>"const idToken = await getAuth(firebaseApp).currentUser.getIdToken();
// use idToken as `Authorization` header value in the format "Bearer <idToken>"To make tests remotely, check what is your remote functions URL: in the Firebase Console go
to Functions > and check the api url, it ends with .cloudfunctions.net/api.
In case you want to make tests locally using the Firebase Emulator,
you can run npm run emulator inside the functions folder.
Open the Emulator UI
on http://127.0.0.1:3005 > Functions emulator > and on the first lines
check the http function initialized... log, it shows your Local URL, it ends with /api.
1. Import the postman_collection.json file to your Postman
2. Right-click on the Postman collection you previously imported, click on Edit > Variables and on api replace the Current Value with your API URL.
Make sure the URL ends with /api and remember that if you use the local
emulator URL it won't affect the remote db.
If you are testing using the local emulator, it will look something like: http://127.0.0.1:<port>/<your-project-id>/<region>/api
But if you are testing using the remote db, it will look like: https://<your-project-id>.cloudfunctions.net/api
3. Create an account on the 1. Create Account Postman Request
4. Follow the login steps to get an ID Token on Postman:
It's better to use a library of Firebase Authentication on the Client Side to get the ID Token, but let's use this method for testing because we are using Postman only
-
4.1. In the Firebase Console > Go to Project Overview and Add a new Web platform
-
4.2. Add a Nickname like "Postman" and click on Register App
-
4.3. Copy only the apiKey field inside the
firebaseConfigobject -
4.4 Let's get the Firebase Authentication Token, on Postman, go to
2. Login on Google APISrequest example and pass theapiKeyas Query Param, edit the body with your email and password and click on Send, you will obtain anidTokenas the response. -
4.5 For the other requests, the
idTokenshould be set in theAuthorizationheader (type Bearer). Let's set it as Postman variable too, so right-click on the Postman collection Edit > Variables and on idToken replace the Current Value with the user idToken you previously obtained.
This project uses custom claims on Firebase Authentication to define which routes the users have access to.
This can be done in the server like below:
await admin.auth().setCustomUserClaims(user.uid, {
storeOwner: true,
buyer: false,
admin: false
});You can set a param (array of strings) on the httpServer.<method>
function, like:
httpServer.get (
'/product/:productId/full-details',
this.getProductByIdFull.bind(this), ['storeOwner']
);In the example above, only users with the storeOwner custom claim will
have access to the /product/:productId/full-details path.
Is this enough? Not always, so let's check the next section Errors and permissions.
You can easily send an HTTP response with code between 400 and 500 to the client
by simply throwing a new HttpResponseError(...) on your controller, service or repository,
for example:
throw new HttpResponseError(400, 'BAD_REQUEST', "Missing 'name' field on the body");Sometimes defining roles isn't enough to ensure that a user can't
access or modify a specific data,
let's imagine if a store owner tries to get full details
of a product he is not selling, like a product of another store owner,
he still has access to the route because of his storeOwner custom claim,
but an additional verification is needed.
if (product.storeOwnerUid != req.auth!.uid) {
throw new HttpResponseError(
403,
'FORBIDDEN',
`You aren't the correct storeOwner`
);
}Means you are not logged in with a user that has the buyer claim rather
than with a user that contains the storeOwner claim.
Means you are logged in with the correct claim, but you are trying to read other storeOwner's data.
Means that this operation requires to be logged with
a user that has the admin claim, but the current user hasn't.
If you forget to add the Authentication header
This project adds 3 new fields to the request object on the
express request handler,
you can also customize this on src/api/@types/express.d.ts TypeScript file.
type: boolean
Is true only if the client is authenticated, which means, the client
informed Authorization on the headers, and these
values were successfully validated.
type: UserRecord | null
If authenticated: Contains user data of Firebase Authentication.
type: DecodedIdToken | null
If authenticated: Contains token data of Firebase Authentication.
Feel free to open a GitHub issue about:
-
❔ questions
-
💡 suggestions
-
🐜 potential bugs
This project used as reference part of the structure of the GitHub project node-typescript-restify. Thank you developer!