Firebase for your Dart Web API
Tags: dart, firebase, flutter, backend
Published: 2022-01-01 06:14:41

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 :

1. Login and Sign-up of new users using Firebase Authentication.
2. 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 !!


![FRIENDS Excited](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rh4tjidmf39e7ispp13o.png)

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)** :

![New Dart Project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l4s28j4gywujdmqwdv6e.png)

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 :

```yaml
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 :

```dart
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:


![Firebase setup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3u6wbrq1d4eljy1bknwh.png)

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:

```dart
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 :

```dart
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.

```dart
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 :

```dart
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.

```dart
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 ?

```dart
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 :

```dart
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 :

```dart
  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 :

```dart
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 :

```dart
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:

```dart
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. 

```dart
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.

```dart
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 :

1. Reading data from Realtime DB
2. Adding new data to the DB
3. Updating existing data in the DB
4. 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 : 

![DB Structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uonmuq7ll5o9gfe8ha8w.png)

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 :

```dart
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 :

![DB URL](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nhodmztmlgjx6nauu90v.png)

Next, head over to your `configurations.dart` file , and paste the url :

```dart
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 :

```dart
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** :

```dart
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.

![DB Structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1fb1vcyauiao5fhmzy7k.png)

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 :

![API Response](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zk1pbsqnmiomfyqhgc1w.png)

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 :

```json
{
  'name':'Gunther',
  'age' : 34
}
```

The handler goes like this :

```dart
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 :

```dart
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 :

```json
{
  "name":"Gunther",
  "age" : 31
}
```

The dart code goes like this:

```dart
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 :
```dart
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 : 

```json
{
  "name":"Gunther"
}
```

The Dart code will be :

```dart
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 :

```dart
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](https://twitter.com/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 !!