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:
- Restful Dart server using alfred
- Client app using Flutter & Riverpod
Other articles in this series:
-
Part Two <--- you are here
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 Method | Endpoint | Description |
---|---|---|
GET | /healthcheck | check if server is up |
GET | /furnitures | list all available furnitures |
GET | /furnitures/:id | details 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
/healthcheck
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"}
/furnitures
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"},....]
/furnitures/:id
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:
- Create a single database instance.
- Create custom Dart handlers for various resources.
- Mitigate possible handlers’ errors.
- 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