Aswin Gopinathan
Posted on January 1, 2022
Yes, you read it right. "Firebase for your Dart Web API"
In this article, i will explain how you can connect your Firebase account with your Dart application and use its features like :
- Login and Sign-up of new users using Firebase Authentication.
- Realtime Database for CRUDing data using Dart-built APIs.
Note: Don't confuse this article on connecting your Firebase account to your Flutter code. When working with Flutter, you have to create separate apps in your Firebase console for Android and iOS. But, when working with Dart application, you dont have to create any new apps!.
Excited ? Of course you are !!
So first things first...Let's create a new Dart-server application.
I will be using IntelliJ as the IDE.
So i will create a New Dart Project (web-server) :
Name the project after your pet doggo, or 'test-server' will also do.
Once the project is created, you will be given the template code, which you don’t have to delete. We will be building on top of that.
But, before we start coding, let's import some packages :
1. shelf_router : https://pub.dev/packages/shelf_router
For routing incoming requests.
2. firebase_dart : https://pub.dev/packages/firebase_dart
The mastermind which is gonna help us play with Firebase using pure Dart.
So, the dependencies goes like this inside pubspec.yaml
. The versions may change depending on which century you are reading this article. In 2021-Dec (21st Century), the versions were the ones given below :
shelf_router: ^1.1.2
firebase_dart: ^1.0.3
Now, before we start writing handler code, let's initialize firebase in our Dart application.
Head over to bin/server.dart
file and inside main()
method, write this line of code :
FirebaseDart.setup();
This initializes the pure dart firebase implementation.
So, that's great. You just added the dependencies and initialized firebase in your server application, now let's create a Configuration file which will contain all the keys required for firebase connectivity.
Create a new file configurations.dart
.
Now, head over to your Firebase console and the easiest way to get all the credentials is to create a sample web app. During the step of creating a new web app for your Firebase project, you will be given the credentials as a json string:
Just copy them and cancel the setup, so that no new apps are created.
Go and paste them in the newly created configurations.dart
as follows:
class Configurations {
static const firebaseConfig = {
'apiKey': '<API_KEY>',
'authDomain': '<AUTH_DOMAIN>',
'projectId': '<PROJECT_ID>',
'storageBucket': '<STORAGE_BUCKET>',
'messagingSenderId': '<ID>',
'appId': '<ID>',
'measurementId': '<ID>'
};
}
Well thats all for that, now let's create a new file for managing the Authentication requests.
Authentication using Firebase-Auth
Create a new file endpoints/auth.dart
and add the following code :
import 'package:shelf_router/shelf_router.dart';
import 'package:shelf/shelf.dart';
class Authentication {
Handler get handler {
var router = Router();
return router;
}
}
Next, we need to write a method inside the Authentication
class which will be initialize the Firebase app in our application.
Future<FirebaseApp> initApp() async{
late FirebaseApp app;
try {
app = Firebase.app();
} catch(e) {
app = await Firebase.initializeApp(
options: FirebaseOptions.fromMap(Configurations.firebaseConfig));
}
return app;
}
Let me break down this code for you.
First, we create a new instance for FirebaseApp. Now, let me give you a brief insight on APIs.
So, what happens in Web APIs is that, every time you hit the API endpoint, the entire code is re-run again and no state is maintained between the calls. Due to which, the code wont remember if we had already initialized the firebase app or not. But, Firebase is quite quirky, it does remember whether its app was already initialized or not.
Now, there is a conflict of interest here. The codebase will re-run the same initialize step irrespective of the app already initialized or not which will lead to the exception : "A Firebase App named "[DEFAULT]" already exists". So, what we do is we use a try-catch block to determine if the app is already initialized, if so, go with that initialized app, or else, initialize a new app with the given credentials stored in the Configuration file.
It's that simple.
Once, we get the app, we return it.
Now, let's write a POST handler for registering new users. For that, first write the following code inside the handler
method :
router.post('/register',(Request request) async{
var payloadData = await request.readAsString();
if(payloadData.isEmpty) {
return Response.notFound(
jsonEncode({'success':false,'error': 'No data found'}),
headers: {'Content-Type': 'application/json'}
);
}
// more code coming your way
});
Here, we wrote a POST handler for the endpoint /register
, and we are expecting data to be sent with the request. The data here could be the email and password of the new user being created.
If the data is not passed, then we return a 404 response saying 'No data found'.
Now, lets continue writing code inside this handler.
final payload = json.decode(payloadData);
String? username = payload['username'];
String? password = payload['password'];
Now, we retrieve the username and password from the payload and store it in a variable.
What if, the requester did not send the username and password fields, instead sent some random string ?
if(username == null || password == null) {
return Response.notFound(
json.encode({'error': 'Missing username or password'}),
headers: {'content-type': 'application/json'}
);
} else if(username.contains(' ')) {
return Response.forbidden(
json.encode({'error': 'Username cannot contain spaces'}),
headers: {'content-type': 'application/json'}
);
}
We validate them and send responses if they are not proper.
Next, lets get the FirebaseApp instance that we are gonna use in our code, and initialize a FirebaseAuth instance using that :
var app = await initApp();
var auth = FirebaseAuth.instanceFor(app: app);
Import the firebase_dart/auth.dart'
package to use FirebaseAuth
class.
Now, let's write a method which will register a new user for us using Firebase Authentication.
Copy the code below and i will explain how it works :
Future<List> registerUser({
String? username,
String? password,
FirebaseAuth? auth,
}) async{
try {
var userCredential = await auth!.createUserWithEmailAndPassword(
email: '$username@company.com'.toLowerCase(),
password: password!,
);
return [1,json.encode({
'username': username,
'uid': userCredential.user!.uid,
'message': 'User created'
})];
} on FirebaseAuthException catch(e) {
print(e.code);
switch(e.code) {
case 'weak-password' :
return [0,json.encode({'error': e.message})];
case 'internal-error':
return [0,json.encode({'error': e.message})];
default:
return [0,json.encode({'error': e.message})];
}
}
}
First things first. Notice this line :
email: '$username@company.com'.toLowerCase(),
I am using a email-authentication approach to create new users using just username. I have appended @company.com
because email authentication requires email as a parameter. toLowerCase()
is used so that Aswin
and aswin
dont get created as separated users.
The try-catch block is necessary to catch exceptions like "User-already-exist", "Weak-password" , etc.
The return type is a list, in which the int in the 0th index signifies success/failure of the registration process, and the second item denotes the json response.
So, this method will register the new users for us. We just have to pass the username, password and the FirebaseAuth instance that we created in the POST handler method.
Now, let's finish the handler code :
var response = await registerUser(
username: username,
password: password,
auth: auth,
);
if(response[0] == 0) {
return Response.notFound(
response[1],
headers: {'content-type': 'application/json'}
);
} else {
return Response.ok(
response[1],
headers: {'content-type': 'application/json'}
);
}
Well, thats Register User API.
Now, let's write a handler method for Login. We create a new handler for the POST method on /login
as follows:
router.post('/login', (Request request) async{
var projectData = await request.readAsString();
if(projectData.isEmpty) {
return Response.notFound(
jsonEncode({'success':false,'error': 'No data found'}),
headers: {'Content-Type': 'application/json'}
);
}
final payload = json.decode(projectData);
String? username = payload['username'];
String? password = payload['password'];
if(username == null || password == null) {
return Response.notFound(
json.encode({'error': 'Missing username or password'}),
headers: {'content-type': 'application/json'}
);
}
else if(username.contains(' ')) {
return Response.forbidden(
json.encode({'error': 'Username cannot contain spaces'}),
headers: {'content-type': 'application/json'}
);
}
var app = await initApp();
var auth = FirebaseAuth.instanceFor(app: app);
// more code coming soon
});
It first checks for the payload data sent via request, and validates it. If no data is send, it returns a 404 Error Response.
If data is sent, we check for the value of username and password and performs validation and for any errors appropriate error responses are returned.
Then if they are correct, we get the FirebaseApp instance and initialize the FirebaseAuth instance using that.
Next, let's write a method to perform the login function.
Future loginUser({
String? username,
String? password,
FirebaseAuth? auth,
}) async{
try {
var userCredential = await auth!.signInWithEmailAndPassword(
email: '$username@doit.com',
password: password!,
);
return [1,json.encode({
'username': username,
'uid': userCredential.user!.uid,
'message': 'User logged in'
})];
} on FirebaseAuthException catch(e) {
print(e.code);
switch(e.code) {
case 'wrong-password' :
return [0,json.encode({'error': e.message})];
case 'user-not-found' :
return [0,json.encode({'error': e.message})];
case 'internal-error':
return [0,json.encode({'error': e.message})];
default:
return [0,json.encode({'error': e.message})];
}
}
}
We use the signInWithEmailAndPassword()
method to sign-in the user using the credentials passed. If any exceptions are raised, they are returned with the flag 0
indicating an error occured.
Now, coming back to the handler method, we call this method and performs action accordingly.
var response = await loginUser(
username: username,
password: password,
auth: auth
);
if(response[0] == 0) {
return Response.notFound(
response[1],
headers: {'content-type': 'application/json'}
);
} else {
return Response.ok(
response[1],
headers: {'content-type': 'application/json'}
);
}
Phew! That's the entire Authentication part of the application.
So, in this section we wrote code for the endpoints /register
and /login
.
Now, lets jump to the next section.
CRUDing Data in Firebase Realtime DB
So, what are we gonna do in this section ?
We are gonna create APIs for :
- Reading data from Realtime DB
- Adding new data to the DB
- Updating existing data in the DB
- Deleting data from the DB
Before we get started, head over to your Firebase Console and create a new Database by choosing the region and rules. Once done, head back to your IDE.
Let's get started...
So, we are gonna use the Realtime DB to store the name and age of characters from the sitcom F.R.I.E.N.D.S
We are gonna be following the given structure for storing the values in the DB :
First, let's create a new file which will contain the class for the APIs. It will be inside the endpoints
folder. We will name it friends.dart
and add the following code to it :
import 'package:shelf_router/shelf_router.dart';
import 'package:shelf/shelf.dart';
import '../configuration.dart';
class Friends {
Handler get handler {
var router = Router();
return router;
}
}
Now, head over to your Realtime Database that you created, and copy the DB URL from the top of the DB :
Next, head over to your configurations.dart
file , and paste the url :
static const databaseUrl = '<DB_URL>';
Now, head back to friends.dart
file. Let's write the code for initializing the Firebase App. We will copy the same code that we had used for Authentication
class :
Future<FirebaseApp> initApp() async{
late FirebaseApp app;
try {
app = Firebase.app();
} catch(e) {
app = await Firebase.initializeApp(
options: FirebaseOptions.fromMap(Configurations.firebaseConfig));
}
return app;
}
Now, let's write a GET request handler for retrieving all the data stored in the DB :
router.get('/all', (request) async {
var app = await initApp();
final db = FirebaseDatabase( app: app, databaseURL: Configurations.databaseUrl);
final ref = db.reference().child('characters');
var responseData;
await ref.once().then((value){
responseData = value.value;
});
return Response.ok(
json.encode(responseData),
headers: {'content-type': 'application/json'}
);
});
Let me break it down for you.
We have defined a handler for the GET
request on the endpoint /all
, which will return all the data stored in the Realtime DB.
First, we initialize and get the FirebaseApp from the initApp()
method. Then we use that instance to create an instance of FirebaseDatabase
by passing the app
instance along with the Database URL
from the Configurations file.
Then, we refer the child characters
which according to the DB Structure contains all the data that needs to be displayed.
We then store them in a variable responseData
and return it as the response for the API call.
The API response will be something like this :
This is because, i have already pre-filled the DB with some data. If the DB does not contain any data, the variable responseData
will contain null
in it.
So, dont forget to add some validations as well.
Now, let's see how we can add new data to the DB.
We will be using a POST
request on the URL /add
and we will be passing the following content as payload :
{
'name':'Gunther',
'age' : 34
}
The handler goes like this :
router.post('/add',(Request request) async{
var projectData = await request.readAsString();
if(projectData.isEmpty) {
return Response.notFound(
jsonEncode({'success':false,'error': 'No data found'}),
headers: {'Content-Type': 'application/json'}
);
}
final payload = jsonDecode(projectData);
final name = payload['name'];
final age = payload['age'];
if(name==null) {
return Response.notFound(
jsonEncode({'success':false,'error': 'Missing name'}),
headers: {'Content-Type': 'application/json'}
);
} else if(age==null) {
return Response.notFound(
jsonEncode({'success':false,'error': 'Missing color'}),
headers: {'Content-Type': 'application/json'}
);
}
final app = await initApp();
final db = FirebaseDatabase( app: app, databaseURL: Configurations.databaseUrl);
final ref = db.reference().child('characters');
await ref.set({
'name': name,
'age': age
});
return Response.ok(
jsonEncode({'success':true}),
headers: {'Content-Type': 'application/json'}
);
});
We retrieve the data sent over the request and validate it for the correctness of the data. If there is any error with the data, appropriate Responses are returned.
If the data is proper, we define a FirebaseDatabase
instance and set the value into the DB. The code that adds new content to the DB is :
final ref = db.reference().child('characters');
await ref.set({
'name': name,
'age': age
});
Next, let's create an API to update existing data in the DB.
We will be using a PUT
request handler on the endpoint /update
to update the age of a particular entry in the DB.
We will pass the following data as the payload and update the age of the person with the new age that is passed :
{
"name":"Gunther",
"age" : 31
}
The dart code goes like this:
router.put('/update',(Request request) async{
var projectData = await request.readAsString();
if(projectData.isEmpty) {
return Response.notFound(
jsonEncode({'success':false,'error': 'No data found'}),
headers: {'Content-Type': 'application/json'}
);
}
final payload = jsonDecode(projectData);
final name = payload['name'];
final age = payload['age'];
if(name==null) {
return Response.notFound(
jsonEncode({'success':false,'error': 'Missing name'}),
headers: {'Content-Type': 'application/json'}
);
} else if(age==null) {
return Response.notFound(
jsonEncode({'success':false,'error': 'Missing color'}),
headers: {'Content-Type': 'application/json'}
);
}
final app = await initApp();
final db = FirebaseDatabase( app: app, databaseURL: Configurations.databaseUrl);
final ref = db.reference().child('characters');
await ref.update({
name : age,
});
return Response.ok(
jsonEncode({'success':true}),
headers: {'Content-Type': 'application/json'}
);
});
We have used the code :
await ref.update({
name : age,
});
to update the age
value of the key name
.
Finally, let's see how we can Delete an existing key from the DB
We will be using a DELETE
request handler on the endpoint /delete
to delete a particular entry from the DB.
The payload will be the name to be deleted :
{
"name":"Gunther"
}
The Dart code will be :
router.delete('/delete',(Request request) async{
var projectData = await request.readAsString();
if(projectData.isEmpty) {
return Response.notFound(
jsonEncode({'success':false,'error': 'No data found'}),
headers: {'Content-Type': 'application/json'}
);
}
final payload = jsonDecode(projectData);
final name = payload['name'];
if(name==null) {
return Response.notFound(
jsonEncode({'success':false,'error': 'Missing name'}),
headers: {'Content-Type': 'application/json'}
);
}
final app = await initApp();
final db = FirebaseDatabase( app: app, databaseURL: Configurations.databaseUrl);
final ref = db.reference().child('characters');
await ref.child(name).remove();
return Response.ok(
jsonEncode({'success':true}),
headers: {'Content-Type': 'application/json'}
);
});
It deletes the child with the given name
.
Well, thats all for the CRUDing of Data in Firebase.
But, before we end this article, there is one important thing that we left out.
How are we gonna access these API endpoints ? In the server.dart
, we can only provide one handler at a time. What to do in order to access other API endpoints ?
We create a starter-handler ! Pardon the name, i am not good at naming stuffs.
So what does this starter-handler
do ?
It creates an entrypoint for your requests, and then based on the endpoint we mount
the request to its appropriate classes.
Let me explain with the actual code :
Create a new file endpoints/starter.dart
and add the following code :
import 'package:shelf_router/shelf_router.dart';
import 'package:shelf/shelf.dart';
import 'auth.dart';
import 'friends.dart';
class Starter {
Handler get handler {
var router = Router();
router.mount('/auth', Authentication().handler);
router.mount('/friends', Friends().handler);
return router;
}
}
So what is happening here ?
We create a Request handler, which will redirect us to its appropriate class based on the endpoint you have mentioned in the URL.
For example :
Earlier while working with Realtime database, we used the endpoint /all
for retrieving all the data from the DB. But, now we have to use /friends/all
to retrieve all the data.
/friends
is telling the server code that we need to head towards the Friends
class and call the endpoint /all
from there.
Similarly, to register new user we need to use the endpoint /auth/register
instead of /register
.
Why do we have to do all this ??
Since we have used separate classes for each of the feature (authentication, db access), we need a router that will redirect us to the appropriate classes. Or else, if we had written all the handlers of Authentication
and Friends
class in a single class, then it wouldnt be an issue. But, Software Engineering is all about Separation of Concern (DeCoupling) right ?
Well, i guess that's it for this article. Feel free to connect with me on Twitter if you have any queries.
Twitter : @GopinathanAswin
You can access the code for this article from the GitHub Repository : https://github.com/infiniteoverflow/Friends_API
See you all in my next article !!
Posted on January 1, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.