DEV Community

Cover image for Firebase for your Dart Web API
Aswin Gopinathan
Aswin Gopinathan

Posted on

Firebase for your Dart Web API

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

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

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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

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>'
  };
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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'];
Enter fullscreen mode Exit fullscreen mode

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'}
    );
 }
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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})];
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

First things first. Notice this line :

email: '$username@company.com'.toLowerCase(),
Enter fullscreen mode Exit fullscreen mode

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'}
  );
}
Enter fullscreen mode Exit fullscreen mode

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

});
Enter fullscreen mode Exit fullscreen mode

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})];
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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'}
  );
}
Enter fullscreen mode Exit fullscreen mode

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

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, head over to your Realtime Database that you created, and copy the DB URL from the top of the DB :

DB URL

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

static const databaseUrl = '<DB_URL>';
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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'}
  );
});
Enter fullscreen mode Exit fullscreen mode

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

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

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
}
Enter fullscreen mode Exit fullscreen mode

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'}
  );
});
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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'}
  );
});
Enter fullscreen mode Exit fullscreen mode

We have used the code :

await ref.update({
  name : age,
});
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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'}
  );

});
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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 !!

Top comments (0)