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
api
folder - Access Control: Reject user access by simply choosing what user roles can access a specific path or easily check the claims with a custom
request
object 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-triggers
folder
- A well-organized Events Triggers under the
-
Shared components between API and Event Triggers are under the
core
folder
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-tools
to install the Firebase CLIfirebase use --add
and 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
firebaseConfig
object -
4.4 Let's get the Firebase Authentication Token, on Postman, go to
2. Login on Google APIS
request example and pass theapiKey
as Query Param, edit the body with your email and password and click on Send, you will obtain anidToken
as the response. -
4.5 For the other requests, the
idToken
should be set in theAuthorization
header (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!