monart_http 0.1.1 copy "monart_http: ^0.1.1" to clipboard
monart_http: ^0.1.1 copied to clipboard

HTTP client foundation for Dart built on monart. Create typed HTTP service objects using Railway-Oriented Programming with semantic outcomes per status code.

monart_http #

pub.dev CI

HTTP client foundation for Dart built on monart. Create typed HTTP service objects where you declare what — verb, path, configuration, and how to map the response — and the base handles how: request dispatch, status-code-to-outcome mapping, and network error handling. Every call returns a Future<Result<Value>>, never throws.

Inspired by the Ruby gem f_http_client.

Documentation #


Installation #

dependencies:
  monart: ^0.1.1
  monart_http: ^0.1.0
import 'package:monart_http/monart_http.dart';

Quick start #

// 1. Declare a shared configuration for each HTTP client
const githubConfig = HttpClientConfiguration(
  baseUri: 'https://api.github.com',
  connectTimeout: Duration(seconds: 5),
  receiveTimeout: Duration(seconds: 15),
  logStrategy: LogStrategy.stdout, // logs to Flutter DevTools in debug
);

// 2. Declare one service object per HTTP operation
class GithubUserFindService extends HttpServiceBase<GithubUser> {
  const GithubUserFindService({required this.username});

  final String username;

  @override
  HttpMethod get method => HttpMethod.get;

  @override
  String get pathTemplate => '/users/:username';

  @override
  Map<String, String> get pathParams => {'username': username};

  @override
  HttpClientConfiguration get configuration => githubConfig;

  @override
  GithubUser fromResponse(HttpResponse response) =>
      GithubUser.fromJson(response.body as Map<String, dynamic>);
}

// 3. Call it
await GithubUserFindService(username: 'dart-lang')
    .runAsync()
    .onSuccessOf('ok', (user) => print(user.name))
    .onFailureOf('notFound', (_) => print('User not found'))
    .onFailureOf('timeout', (_) => print('Request timed out'))
    .onFailure((outcome, _) => print('Unexpected: $outcome'));

Usage #

HttpClientConfiguration #

Define a configuration once per HTTP client and share it across all its service objects:

const marketListApiConfig = HttpClientConfiguration(
  baseUri: 'https://api.marketlist.example.com',
  defaultHeaders: {'Accept': 'application/json'},
  connectTimeout: Duration(seconds: 10),
  receiveTimeout: Duration(seconds: 30),
  logStrategy: LogStrategy.stdout,
);
Field Default Description
baseUri required Base URL prepended to every path
defaultHeaders {} Headers sent on every request
connectTimeout 10s Time to establish connection
receiveTimeout 30s Time to receive the full response
logStrategy none stdout enables logging to Flutter DevTools

HttpServiceBase #

Subclass HttpServiceBase<Value> and declare the five required members:

class OrderCreateService extends HttpServiceBase<Order> {
  const OrderCreateService({required this.order});

  final Order order;

  @override
  HttpMethod get method => HttpMethod.post;

  @override
  String get pathTemplate => '/orders';

  @override
  HttpClientConfiguration get configuration => marketListApiConfig;

  @override
  Object? get body => order.toJson();

  @override
  Order fromResponse(HttpResponse response) =>
      Order.fromJson(response.body as Map<String, dynamic>);
}

Path parameters

Use :paramName placeholders in pathTemplate and provide values in pathParams:

class OrderFindService extends HttpServiceBase<Order> {
  const OrderFindService({required this.orderId});

  final String orderId;

  @override
  HttpMethod get method => HttpMethod.get;

  @override
  String get pathTemplate => '/orders/:orderId';

  @override
  Map<String, String> get pathParams => {'orderId': orderId};

  @override
  HttpClientConfiguration get configuration => marketListApiConfig;

  @override
  Order fromResponse(HttpResponse response) =>
      Order.fromJson(response.body as Map<String, dynamic>);
}

Query parameters

Override queryParams to append query string parameters:

@override
Map<String, dynamic> get queryParams => {'page': page, 'per_page': perPage};

Per-request headers

Override headers to add headers that take precedence over defaultHeaders:

@override
Map<String, String> get headers => {'Authorization': 'Bearer $token'};

Outcomes #

runAsync() returns a Future<Result<Value>>. On success the result carries the value from fromResponse. On a failed HTTP status or network error the result is a Failure.

HTTP status codes are mapped to semantic outcome names. Most codes resolve to two outcomes — a specific name and a family name:

Status Outcomes
200 ['ok', 'successful']
201 ['created', 'successful']
204 ['noContent', 'successful']
400 ['badRequest', 'clientError']
401 ['unauthorized', 'clientError']
403 ['forbidden', 'clientError']
404 ['notFound', 'clientError']
409 ['conflict', 'clientError']
422 ['unprocessableContent', 'clientError']
429 ['tooManyRequests', 'clientError']
500 ['internalServerError', 'serverError']
503 ['serviceUnavailable', 'serverError']

You can match either outcome in onSuccessOf / onFailureOf:

await OrderCreateService(order: order)
    .runAsync()
    .onSuccessOf('created', (order) => navigateToOrder(order))
    .onFailureOf('unprocessableContent', (_, response) => showValidationErrors(response))
    .onFailureOf('clientError', (_, __) => showGenericClientError())
    .onFailureOf('timeout', (_) => showTimeoutBanner())
    .onFailure((outcome, _) => logger.error('Unexpected: $outcome'));

Network errors map to:

Exception Outcome
Connection / send / receive timeout timeout
SSL certificate failure sslError
No network / refused connection connectionError
Request cancelled requestCancelled
Any other exception uncaughtError

Logging #

Set logStrategy: LogStrategy.stdout on a configuration to enable request/response logging. Logs are sent to the Flutter DevTools Logging tab via dart:developer — they never appear in production builds.

→ GET https://api.example.com/users/42
← 200 OK (31ms)
  {id: 42, name: Alice}

→ POST https://api.example.com/sessions
  {email: alice@example.com, password: secret}
← 422 Unprocessable Content (18ms)
  {errors: {email: [has already been taken]}}

✗ GET https://api.example.com/orders — connectionError (0ms)

Testing #

Use http_mock_adapter to stub HTTP responses in tests. Every HttpServiceBase exposes a testDio getter that returns the internal Dio instance:

import 'package:http_mock_adapter/http_mock_adapter.dart';
import 'package:monart/monart_testing.dart';

void main() {
  late GithubUserFindService service;
  late DioAdapter adapter;

  setUp(() {
    service = const GithubUserFindService(username: 'dart-lang');
    adapter = DioAdapter(dio: service.testDio);
  });

  it('succeeds with the user on 200', () async {
    adapter.onGet('/users/dart-lang', (server) => server.reply(200, {'login': 'dart-lang'}));

    expect(
      await service.runAsync(),
      haveSucceededWith(['ok', 'successful']),
    );
  });

  it('fails with notFound on 404', () async {
    adapter.onGet('/users/dart-lang', (server) => server.reply(404, null));

    expect(
      await service.runAsync(),
      haveFailedWith(['notFound', 'clientError']),
    );
  });
}

testDio is annotated @visibleForTesting — it only appears in test files and does not form part of the public API.


Contributing #

Running tests and analysis locally #

dart pub get
dart analyze --fatal-infos
dart test

Workflows #

CI runs automatically on every push to master and on every pull request. Tests and static analysis are executed against three SDK versions — 3.1.0 (the declared minimum), stable, and beta. Only stable is required to pass; the other two run with continue-on-error.

Release is triggered by pushing a version tag:

git tag v0.1.0
git push origin v0.1.0

The workflow runs the full test suite and creates a GitHub Release with auto-generated release notes.

Docs are deployed to GitHub Pages automatically when the version: line in pubspec.yaml changes on master. To force a deploy, go to Actions → "Deploy Docs" → "Run workflow".

Publish is always manual. After the release tag exists, go to Actions → "Publish to pub.dev" → "Run workflow", enter the version (e.g. 0.1.0), and confirm. The workflow checks out that exact tag, re-runs tests and analysis, does a --dry-run, and only then publishes.

Publishing uses OIDC (Trusted Publishers) — no tokens stored as secrets. First-time setup: on pub.dev go to "My pub.dev" → "Trusted publishers" and add github.com/bvicenzo/monart_http.


License #

MIT

0
likes
0
points
204
downloads

Publisher

unverified uploader

Weekly Downloads

HTTP client foundation for Dart built on monart. Create typed HTTP service objects using Railway-Oriented Programming with semantic outcomes per status code.

Repository (GitHub)
View/report issues

Topics

#http #networking #railway-oriented-programming #service-objects

License

unknown (license)

Dependencies

dio, http_status, meta, monart

More

Packages that depend on monart_http