A comprehensive guide to using the CQRS package for backend communication with queries and commands
The CQRS package provides a convenient way to communicate with CQRS-compatible backends using queries and commands. This guide will walk you through setting up and using CQRS in your Flutter application.
CQRS (Command Query Responsibility Segregation) is a pattern that separates read operations (queries) from write operations (commands). The CQRS package makes it easy to:
Execute queries to fetch data from your backend
Run commands to perform actions and modify data
Handle validation errors and command results
Manage authentication with automatic token refresh
First, create a CQRS instance with your API endpoint and an HTTP client that handles authentication:
import 'package:cqrs/cqrs.dart';import 'package:login_client/login_client.dart';// Create your API URI with a trailing slashfinal apiUri = Uri.parse('https://api.example.com/api/');// Set up LoginClient for authentication (see Authentication Setup guide)final loginClient = LoginClient( oAuthSettings: OAuthSettings( authorizationUri: apiUri.resolve('/auth'), clientId: 'your_client_id', ), credentialsStorage: const FlutterSecureCredentialsStorage(),);// Initialize CQRS with the authenticated clientfinal cqrs = Cqrs( loginClient, apiUri, timeout: const Duration(seconds: 30), headers: {'Content-Type': 'application/json'},);
Make sure your API URI includes the trailing slash to ensure requests are sent to the correct endpoints.
Query results return a QueryResult<T> which can be either:
QuerySuccess
QueryFailure
final result = await cqrs.get(AllFlowers(page: 1));if (result case QuerySuccess(:final data, :final rawBody)) { // Access the deserialized data print(data); // Access the raw response body if needed print(rawBody);}
if (result case QueryFailure(:final error)) { switch (error) { case QueryError.network: print('Network error occurred'); case QueryError.authentication: print('Authentication failed'); case QueryError.authorization: print('Not authorized'); case QueryError.jsonDeserialization: print('Failed to parse response'); case QueryError.unknown: print('Unknown error'); }}
Command results return a CommandResult which can be either:
CommandSuccess
CommandFailure
final result = await cqrs.run(AddFlower(name: 'Rose', pretty: true));if (result.isSuccess) { // Command executed successfully showSuccessMessage();}
if (result case CommandFailure(:final error, :final validationErrors)) { // Check if it's a validation error if (result.isInvalid) { // Handle validation errors for (final validationError in validationErrors) { print('${validationError.propertyName}: ${validationError.message}'); } } // Check for specific error codes if (result.hasError(1001)) { print('Error code 1001 found'); } // Check for property-specific errors if (result.hasErrorForProperty(1002, 'Name')) { print('Name field has error 1002'); }}
Future<void> submitForm(String name, bool isPretty) async { final result = await cqrs.run( AddFlower(name: name, pretty: isPretty), ); if (result.isSuccess) { navigateToSuccess(); } else if (result.isInvalid) { // Show validation errors next to form fields final errors = (result as CommandFailure).validationErrors; for (final error in errors) { if (error.propertyName == 'Name') { nameFieldController.error = error.message; } } } else { showErrorDialog('Failed to add flower'); }}