๐ Todos
Overview
Section titled โOverviewโIn this tutorial, weโre going to build an app that exposes two endpoints which
allow us to perform CRUD operations on a list of todos.
When weโre done, we should have an app that supports the following requests:
# Create a new todocurl --request POST \ --url http://localhost:8080/todos \ --header 'Content-Type: application/json' \ --data '{ "title": "Take out trash"}'
# Read all todoscurl --request GET \ --url http://localhost:8080/todos
# Read a specific todo by idcurl --request GET \ --url http://localhost:8080/todos/<id>
# Update a specific todo by idcurl --request PUT \ --url http://localhost:8080/todos/<id> \ --header 'Content-Type: application/json' \ --data '{ "title": "Take out trash!", "isCompleted": true}'
# Delete a specific todo by idcurl --request DELETE \ --url http://localhost:8080/todos/<id>Creating a new app
Section titled โCreating a new appโTo create a new Dart Frog app, open your terminal, cd into the directory where
youโd like to create the app, and run the following command:
dart_frog create todosYou should see an output similar to:
โ Creating todos (0.1s)โ Installing dependencies (1.7s)
Created todos at ./todos.
Get started by typing:
cd ./todosdart_frog devRunning the development server
Section titled โRunning the development serverโYou should now have a directory called todos โ cd into it:
cd todosThen, run the following command:
dart_frog devThis will start the development server on port 8080:
โ Running on http://localhost:8080 (1.3s)The Dart VM service is listening on http://127.0.0.1:8181/YKEF_nbwOpM=/The Dart DevTools debugger and profiler is available at: http://127.0.0.1:8181/YKEF_nbwOpM=/devtools/#/?uri=ws%3A%2F%2F127.0.0.1%3A8181%2FYKEF_nbwOpM%3D%2Fws[hotreload] Hot reload is enabled.Make sure itโs working by opening http://localhost:8080
in your browser or via cURL:
curl --request GET \ --url http://localhost:8080If everything succeeded, you should see Welcome to Dart Frog!.
Todos Data Source
Section titled โTodos Data SourceโCreating package:todos_data_source
Section titled โCreating package:todos_data_sourceโNow that we have a running application, we need to define an abstraction for a todos data source which will be responsible for exposing APIs to perform C.R.U.D operations on a list of todos.
Since the todos data source is not tightly coupled to our Dart Frog application, we can create it as a package.
In this tutorial, weโre going to use package:mason_cli to help us create new packages quickly.
Install the latest version of the Very Good Dart Package by running:
mason add -g very_good_dart_packageThen we can create the todos_data_source via:
mason make very_good_dart_package --project_name "todos_data_source" --description "A generic interface for managing todos." -o packages/todos_data_sourceNow we should have the scaffolding for the todos_data_source package under
packages/todos_data_source:
โโโ packagesโ โโโ todos_data_sourceโ โโโ README.mdโ โโโ analysis_options.yamlโ โโโ coverage_badge.svgโ โโโ libโ โโโ pubspec.lockโ โโโ pubspec.yamlโ โโโ testUpdating the pubspec.yaml
Section titled โUpdating the pubspec.yamlโNext, letโs update the pubspec.yaml in the todos_data_source to include the
relevant dependencies:
name: todos_data_sourcedescription: A generic interface for managing todos.version: 0.1.0+1publish_to: none
environment: sdk: '>=3.0.0 <4.0.0'
dependencies: equatable: ^2.0.3 json_annotation: ^4.6.0 meta: ^1.7.0
dev_dependencies: build_runner: ^2.2.0 json_serializable: ^6.3.1 mocktail: ^1.0.0 test: ^1.19.2 very_good_analysis: ^5.0.0Install the newly added dependencies via:
dart pub get:::caution Make sure to run the above command from within the
packages/todos_data_source directory. :::
Creating the Todo model
Section titled โCreating the Todo modelโNext, letโs define our todo model which will be a plain Dart class which represents a single todo item.
Create lib/src/models/todo.dart with the following contents:
import 'package:equatable/equatable.dart';import 'package:json_annotation/json_annotation.dart';import 'package:meta/meta.dart';
part 'todo.g.dart';
/// {@template todo}/// A single todo item.////// Contains a [title], [description] and [id], in addition to a [isCompleted]/// flag.////// If an [id] is provided, it cannot be empty. If no [id] is provided, one/// will be generated.////// [Todo]s are immutable and can be copied using [copyWith], in addition to/// being serialized and deserialized using [toJson] and [fromJson]/// respectively./// {@endtemplate}@immutable@JsonSerializable()class Todo extends Equatable { /// {@macro todo} Todo({ this.id, required this.title, this.description = '', this.isCompleted = false, }) : assert(id == null || id.isNotEmpty, 'id cannot be empty');
/// The unique identifier of the todo. /// /// Cannot be empty. final String? id;
/// The title of the todo. /// /// Note that the title may be empty. final String title;
/// The description of the todo. /// /// Defaults to an empty string. final String description;
/// Whether the todo is completed. /// /// Defaults to `false`. final bool isCompleted;
/// Returns a copy of this todo with the given values updated. /// /// {@macro todo} Todo copyWith({ String? id, String? title, String? description, bool? isCompleted, }) { return Todo( id: id ?? this.id, title: title ?? this.title, description: description ?? this.description, isCompleted: isCompleted ?? this.isCompleted, ); }
/// Deserializes the given `Map<String, dynamic>` into a [Todo]. static Todo fromJson(Map<String, dynamic> json) => _$TodoFromJson(json);
/// Converts this [Todo] into a `Map<String, dynamic>`. Map<String, dynamic> toJson() => _$TodoToJson(this);
@override List<Object?> get props => [id, title, description, isCompleted];}Next, we need to use
package:build_runner to generate the
relevant code for json_serializable:
dart run build_runner build --delete-conflicting-outputsWe should see that the todos.g.dart file was generated and our code should not
have any errors or warnings at this point.
Letโs add a barrel file to export our models by creating
lib/src/models/models.dart and exporting todos.dart:
export 'todo.dart';Also, letโs update the library exports to include the models in
lib/todos_data_source.dart:
/// A generic interface for managing todos.library todos_data_source;
export 'src/models/models.dart';export 'src/todos_data_source.dart';Thatโs it for the models. Next, weโll define the TodosDataSource class.
Creating the TodosDataSource
Section titled โCreating the TodosDataSourceโThe last thing we need to do in the todos_data_source package is define the
TodosDataSource class. Itโs going to be an abstract class because it will
serve as an interface which can have multiple concrete implementations.
Create lib/src/todos_data_source.dart with the following contents:
import 'package:todos_data_source/todos_data_source.dart';
/// An interface for a todos data source./// A todos data source supports basic C.R.U.D operations./// * C - Create/// * R - Read/// * U - Update/// * D - Deleteabstract class TodosDataSource { /// Create and return the newly created todo. Future<Todo> create(Todo todo);
/// Return all todos. Future<List<Todo>> readAll();
/// Return a todo with the provided [id] if one exists. Future<Todo?> read(String id);
/// Update the todo with the provided [id] to match [todo] and /// return the updated todo. Future<Todo> update(String id, Todo todo);
/// Delete the todo with the provided [id] if one exists. Future<void> delete(String id);}Weโre done with the todos_data_source! Next, weโll create a concrete
implementation of the TodosDataSource interface which is backed by an
in-memory cache.
In-Memory Todos Data Source
Section titled โIn-Memory Todos Data SourceโJust like with the todos_data_source, weโll create a new package called
in_memory_todos_data_source to contain the concrete implementation.
Creating package:in_memory_todos_data_source
Section titled โCreating package:in_memory_todos_data_sourceโFrom the root of the project we can use mason make to generate a new Dart
package again:
mason make very_good_dart_package --project_name "in_memory_todos_data_source" --description "An in-memory implementation of the TodosDataSource interface." -o packages/in_memory_todos_data_sourceUpdating the pubspec.yaml
Section titled โUpdating the pubspec.yamlโNext, letโs update the pubspec.yaml in the in_memory_todos_data_source to
include the relevant dependencies:
name: in_memory_todos_data_sourcedescription: An in-memory implementation of the TodosDataSource interface.version: 0.1.0+1publish_to: none
environment: sdk: '>=3.0.0 <4.0.0'
dependencies: todos_data_source: path: ../todos_data_source uuid: ^3.0.6
dev_dependencies: mocktail: ^1.0.0 test: ^1.19.2 very_good_analysis: ^5.0.0Install the newly added dependencies via:
dart pub getCreating the InMemoryTodosDataSource
Section titled โCreating the InMemoryTodosDataSourceโNext, letโs update lib/src/in_memory_todos_data_source.dart to implement the
TodosDataSource interface:
import 'package:todos_data_source/todos_data_source.dart';import 'package:uuid/uuid.dart';
/// An in-memory implementation of the [TodosDataSource] interface.class InMemoryTodosDataSource implements TodosDataSource { /// Map of ID -> Todo final _cache = <String, Todo>{};
@override Future<Todo> create(Todo todo) async { final id = const Uuid().v4(); final createdTodo = todo.copyWith(id: id); _cache[id] = createdTodo; return createdTodo; }
@override Future<List<Todo>> readAll() async => _cache.values.toList();
@override Future<Todo?> read(String id) async => _cache[id];
@override Future<Todo> update(String id, Todo todo) async { return _cache.update(id, (value) => todo); }
@override Future<void> delete(String id) async => _cache.remove(id);}Thatโs it! Weโre done making the data sources for our Dart Frog application and weโre ready to start working on the Dart Frog app itself!
Updating the pubspec.yaml
Section titled โUpdating the pubspec.yamlโThe first thing we need to do is update the root pubspec.yaml to contain the
todos_data_source and in_memory_todos_data_source dependencies:
name: todosdescription: An example todos app built with Dart Frog.version: 1.0.0+1publish_to: none
environment: sdk: '>=3.0.0 <4.0.0'
dependencies: dart_frog: ^1.0.0 in_memory_todos_data_source: path: packages/in_memory_todos_data_source todos_data_source: path: packages/todos_data_source
dev_dependencies: http: ^1.0.0 mocktail: ^1.0.0 test: ^1.19.2 very_good_analysis: ^5.0.0Install the newly added dependencies via:
dart pub getCreating middleware
Section titled โCreating middlewareโNext, letโs create a top-level piece of middleware to provide the
TodosDataSource to all routes. Create routes/_middleware.dart with the
following contents:
import 'package:dart_frog/dart_frog.dart';import 'package:in_memory_todos_data_source/in_memory_todos_data_source.dart';
final _dataSource = InMemoryTodosDataSource();
Handler middleware(Handler handler) { return handler .use(requestLogger()) .use(provider<TodosDataSource>((_) => _dataSource));}Creating the /todos route
Section titled โCreating the /todos routeโNext, delete the root route handler at routes/index.dart and create a route
handler for the /todos endpoint by creating routes/todos/index.dart:
import 'dart:async';import 'dart:io';
import 'package:dart_frog/dart_frog.dart';import 'package:todos_data_source/todos_data_source.dart';
FutureOr<Response> onRequest(RequestContext context) async { switch (context.request.method) { case HttpMethod.get: return _get(context); case HttpMethod.post: return _post(context); case HttpMethod.delete: case HttpMethod.head: case HttpMethod.options: case HttpMethod.patch: case HttpMethod.put: return Response(statusCode: HttpStatus.methodNotAllowed); }}
Future<Response> _get(RequestContext context) async { final dataSource = context.read<TodosDataSource>(); final todos = await dataSource.readAll(); return Response.json(body: todos);}
Future<Response> _post(RequestContext context) async { final dataSource = context.read<TodosDataSource>(); final todo = Todo.fromJson( await context.request.json() as Map<String, dynamic>, );
return Response.json( statusCode: HttpStatus.created, body: await dataSource.create(todo), );}In this route handler, we only want to handle GET and POST requests so weโre
using a switch statement on context.request.method. If the HttpMethod is
not GET or POST, our route handler responds with a 405 status code (method
not allowed).
In addition, weโre using the Response.json constructor to respond with
Content-Type: application/json.
Next, weโll create a route handler for the /todos/<id> endpoint so that we can
handle operations for a specific todo.
Creating the /todos/<id> route
Section titled โCreating the /todos/<id> routeโWe can create a dynamic route to handle matching and id by creating a file
called: routes/todos/[id].dart.
import 'dart:async';import 'dart:io';
import 'package:dart_frog/dart_frog.dart';import 'package:todos_data_source/todos_data_source.dart';
FutureOr<Response> onRequest(RequestContext context, String id) async { final dataSource = context.read<TodosDataSource>(); final todo = await dataSource.read(id);
if (todo == null) { return Response(statusCode: HttpStatus.notFound, body: 'Not found'); }
switch (context.request.method) { case HttpMethod.get: return _get(context, todo); case HttpMethod.put: return _put(context, id, todo); case HttpMethod.delete: return _delete(context, id); case HttpMethod.head: case HttpMethod.options: case HttpMethod.patch: case HttpMethod.post: return Response(statusCode: HttpStatus.methodNotAllowed); }}
Future<Response> _get(RequestContext context, Todo todo) async { return Response.json(body: todo);}
Future<Response> _put(RequestContext context, String id, Todo todo) async { final dataSource = context.read<TodosDataSource>(); final updatedTodo = Todo.fromJson( await context.request.json() as Map<String, dynamic>, ); final newTodo = await dataSource.update( id, todo.copyWith( title: updatedTodo.title, description: updatedTodo.description, isCompleted: updatedTodo.isCompleted, ), );
return Response.json(body: newTodo);}
Future<Response> _delete(RequestContext context, String id) async { final dataSource = context.read<TodosDataSource>(); await dataSource.delete(id); return Response(statusCode: HttpStatus.noContent);}Just like in the /todos route handler, we are switching on the
context.request.method and selectively handling GET, PUT, and DELETE
requests.
Summary
Section titled โSummaryโBe sure to save all the changes and hot reload should kick in โก๏ธ
[hotreload] - Application reloaded.You should now be able to make requests to create, read, update, and delete todos:
# Create a new todocurl --request POST \ --url http://localhost:8080/todos \ --header 'Content-Type: application/json' \ --data '{ "title": "Take out trash"}'
# Read all todoscurl --request GET \ --url http://localhost:8080/todos
# Read a specific todo by idcurl --request GET \ --url http://localhost:8080/todos/<id>
# Update a specific todo by idcurl --request PUT \ --url http://localhost:8080/todos/<id> \ --header 'Content-Type: application/json' \ --data '{ "title": "Take out trash!", "isCompleted": true}'
# Delete a specific todo by idcurl --request DELETE \ --url http://localhost:8080/todos/<id>๐ Congrats, youโve created a todos application using Dart Frog. View the
full source code.