Skip to content

Fullstack Furniture App with Dart and Flutter Part Two

Posted on:May 8, 2023 at 08:00 PM

image

Flutter baked is an open source project that uses Flutter/Dart to build corss-platform applications with various tech stacks.

Table of contents

Open Table of contents

Project Description

furni is a furniture app which consists of the following:

  1. Restful Dart server using alfred
  2. Client app using Flutter & Riverpod

Prologue

In the previous episode we started to build our Dart server and left out on creating HTTP handlers which we will touch upon in this article.

Database instance injection

Our HTTP handlers need an instance of our database object to be able to call different querying methods.

Before supplying an instance of the DB to our HTTP handlers, it is good practice to ensure we will end up having only one instance of our database model during the lifetime of the application, so let’s refactor MockDB to be a singleton like so.


class MockDB {
  late Map<String, dynamic> parsedJson;

  static MockDB? _instance;

  MockDB._() {
    final jsonString =
        File('${Directory.current.path}/../../db/mock.json').readAsStringSync();

    parsedJson = JsonDecoder().convert(jsonString);
  }

  factory MockDB.init() {
    if (_instance == null) {
      _instance = MockDB._();
    }
    return _instance!;
  }

  FurnitureItemDetails getFurnitureItemDetails(int id) {
    return FurnitureItemDetails.fromJson((parsedJson['funritureList'] as List)
        .firstWhere((item) => item['id'] == id));
  }

  List<FurnitureListItem> getAllFurnitures() {
    final result = (parsedJson['funritureList'] as List).map((item) {
      return FurnitureListItem.fromJson(item);
    }).toList();
    return result;
  }
}

Dart HTTP handlers using alfred

We want to end up having the following endpoints

HTTP MethodEndpointDescription
GET/healthcheckcheck if server is up
GET/furnitureslist all available furnitures
GET/furnitures/:iddetails of a specific furniture item

For now, we can refactor our main function such that it intializes our single MockDB instance and pass it to our different handlers that we will create them later.

void main() {
  final app = Alfred();
  final db = MockDB.init();

  app.get(
    '/healthcheck',
    healthcheckHandler,
  );
  app.get(
    '/furnitures',
    (req, res) => getFuntiruresHandler(req, res, db),
  );
  app.get(
    '/furnitures/:id',
    (req, res) => getFurnitureDetailsHandler(req, res, db),
  );

  app.listen(4242);
}

Now, let’s start creating custom handlers getFuntiruresHandler and getFurnitureDetailsHandler that accepts an HttpRequest, HttpResponse and an extra MockDB instance. Inside our handlers.dart file we can add the following:

FutureOr<dynamic> getFuntiruresHandler(
    HttpRequest req, HttpResponse resp, MockDB db) {
  final furnitureList = db.getAllFurnitures();
  resp.statusCode = 200; // set the status code
  return json.encode({'furnitures': furnitureList});
}

FutureOr<dynamic> getFurnitureDetailsHandler(
    HttpRequest req, HttpResponse resp, MockDB db) {
  final furnitureItem = db.getFurnitureItemDetails(int.parse(req.params['id']));
  resp.statusCode = 200; // set the status code
  return json.encode(furnitureItem.toJson());
}

Testing endpoints

The previous section allowed us to integrate our MockDB with custome handlers, so let’s now test if everything is working as expected. From a terminal inside the lib folder. Let’s run our server.

 dart run main.dart
2023-05-07 11:41:51.862786 - info - HTTP Server listening on port 4242

From another terminal let’s try to hit all of the three handlers we currently have

curl -i localhost:4242/healthcheck
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
transfer-encoding: chunked
x-content-type-options: nosniff

{"status":"available"}
curl -i localhost:4242/furnitures
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
transfer-encoding: chunked
x-content-type-options: nosniff

[{"type":1,"name":"Ante","price":713.59,"imageUrl":"https://source.unsplash.com/collection/1163637/480x480"},{"type":5,"name":"Sagittis","price":1938.66,"imageUrl":"https://source.unsplash.com/collection/1163637/480x480"},{"type":1,"name":"Hac","price":9863.97,"imageUrl":"https://source.unsplash.com/collection/1163637/480x480"},{"type":2,"name":"Amet","price":5728.56,"imageUrl":"https://source.unsplash.com/collection/1163637/480x480"},{"type":2,"name":"Leo","price":7163.9,"imageUrl":"https://source.unsplash.com/collection/1163637/480x480"},....]
curl -i localhost:4242/furnitures/1
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
transfer-encoding: chunked
x-content-type-options: nosniff

{"id":1,"type":1,"name":"Ante","price":713.59,"style":"scandinavian","height":45.09,"width":8.57,"depth":28.81,"imageUrl":"https://source.unsplash.com/collection/1163637/480x480"}

Looks like everything is working as expected, but there is one other thing we can do to enhance our getFurnitureDetailsHandler in regards to validating the path params id.

Validating path param

Currently if the client tries to pass an invalid path param to the /furnitures/:id resource, our getFurnitureDetailsHandler does not include any mechanism to mitigate this scenerio, so for example try to call our endpoint with an id that does not exist

curl -i localhost:4242/furnitures/0
HTTP/1.1 500 Internal Server Error
content-type: text/plain; charset=utf-8
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
transfer-encoding: chunked
x-content-type-options: nosniff

Bad state: No element

Even worse, try to pass an id that is not actually an integer value

curl -i localhost:4242/furnitures/a
HTTP/1.1 500 Internal Server Error
content-type: text/plain; charset=utf-8
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
transfer-encoding: chunked
x-content-type-options: nosniff

FormatException: Invalid radix-10 number (at character 1)
a
^

Instead of this badly formatted responses. We would want to send a 404 Not Found status message to the client in case the provided id does not exist in the database. In addition, we will send a 400 Bad Request status message in case the provided id is not an actual integer. We can almost mitigate this by refactoring our getFurnitureDetailsHandler as follows:

FutureOr<dynamic> getFurnitureDetailsHandler(
    HttpRequest req, HttpResponse resp, MockDB db) {
  final strId = req.params['id'];
  final id = int.tryParse(strId);

  if (id == null) {
    // this means that the provided id is not an actual int
    resp.statusCode =
        400; // letting the client know that they have sent a bad request
    return json.encode({
      'message': 'could not parse $strId as int'
    }); // send an empty json response, you can also customize this as fits
  }

  if (id <= 0) {
    // ids usually are auto increment values that starts from 1
    resp.statusCode = 404;
    return json.encode({'message': 'could not find furniture item of id = $id'});
  }

  final furnitureItem = db.getFurnitureItemDetails(id);
  resp.statusCode = 200; // set the status code
  return json.encode(furnitureItem.toJson());
}
curl -i localhost:4242/furnitures/a
HTTP/1.1 400 Bad Request
content-type: application/json; charset=utf-8
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
transfer-encoding: chunked
x-content-type-options: nosniff

{"message":"could not parse a as int"}
curl -i localhost:4242/furnitures/0
HTTP/1.1 404 Not Found
content-type: application/json; charset=utf-8
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
transfer-encoding: chunked
x-content-type-options: nosniff

{"message":"could not find furniture item of id = 0"}

This is great and all. But there is one thing we still need to do. We will demonstrate the problem by hitting our /furnitures/:id endpoint with an id that does not exist in the list retieved from the database, for example:

curl -i localhost:4242/furnitures/1000
HTTP/1.1 500 Internal Server Error
content-type: text/plain; charset=utf-8
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
transfer-encoding: chunked
x-content-type-options: nosniff

Bad state: No element

This is due to the way we are returning our item model from the data base

FurnitureItemDetails getFurnitureItemDetails(int id) {
    return FurnitureItemDetails.fromJson((parsedJson['funritureList'] as List)
        .firstWhere((item) => item['id'] == id));
  }

Notice that firstWhere will throw if we do not specifiy an orElse callback that will be triggered if the item we are looking for does not exist. So we will need to modify our getFurnitureItemDetails method in our MockDB instance as follows:

FurnitureItemDetails? getFurnitureItemDetails(int id) {
    final jsonItem = (parsedJson['funritureList'] as List).firstWhere(
      (item) => item['id'] == id,
      orElse: () => null,
    );

    if (jsonItem == null) return null;

    return FurnitureItemDetails.fromJson(jsonItem);
  }

Now we can finialize our getFurnitureDetailsHandler to accomedate the changes

FutureOr<dynamic> getFurnitureDetailsHandler(
    HttpRequest req, HttpResponse resp, MockDB db) {
  final strId = req.params['id'];
  final id = int.tryParse(strId);

  if (id == null) {
    // this means that the provided id is not an actual int
    resp.statusCode =
        400; // letting the client know that they have sent a bad request
    return json.encode({
      'message': 'could not parse $strId as int'
    }); // send an empty json response, you can also customize this as fits
  }

  if (id <= 0) {
    // ids usually are auto increment values that starts from 1
    resp.statusCode = 404;
    return json.encode({'message': 'could not find furniture item of id = $id'});
  }

  final furnitureItem = db.getFurnitureItemDetails(id);

  if (furnitureItem == null) {
    // if we can not find the requested item in the database
    resp.statusCode = 404;
    return json.encode({'message': 'could not find furniture item of id = $id'});
  }

  resp.statusCode = 200; // set the status code
  return json.encode(furnitureItem.toJson());
}

Restart the server and let’s give our modified endpoint a final test

curl -i localhost:4242/furnitures/1000
HTTP/1.1 404 Not Found
content-type: application/json; charset=utf-8
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
transfer-encoding: chunked
x-content-type-options: nosniff

{"message":"could not find furniture item of id = 1000"}

Conclusion

In this article we learned how to:

  1. Create a single database instance.
  2. Create custom Dart handlers for various resources.
  3. Mitigate possible handlers’ errors.
  4. Handling malformed data sent from client.

Next Steps

In the next article we will be moving on to work on our Flutter application that consumes our REST api to display a list of furniture and the details of a specific furniture item.

If you like this project, consider staring us on Github

Previous Articles

Fullstack Furniture App with Dart and Flutter Part One