Generate a Dart and Flutter SDK from your OpenAPI spec

Octri reads your OpenAPI document and writes a Dart package with typed models for Flutter apps and Dart servers, publishes it to pub.dev under your own package name, and rebuilds it every time the spec changes.

Install from pub.dev
flutter pub add acme_api

The client they actually get

What your users write once the Dart package is installed.

Two call-site shapes come out of the same spec. class-namespaced is the default: a client object whose operations are grouped by resource, which is the surface every widely used SDK ships and the one your own reference docs already assume. functions is the override: no client object, one configure call, and each operation called directly. It is a single setting, per language, and nothing else about the client changes with it.

Parameters are named, and every call returns a Future or a Stream, so it drops into a FutureBuilder or a StreamBuilder in a Flutter widget without an adapter.

Published to pub.dev under your own publisher. The same package installs with dart pub add in a server or CLI and flutter pub add in an app.

Client object (default)
import 'package:acme_api/acme_api.dart';

final acme = Acme(ClientConfig(
  baseUrl: 'https://api.acme.com/v1',
  auth: ClientAuthConfig(bearer: token),
));

// listPaginated is a lazy Stream. await for asks for the next page on demand.
await for (final user in acme.users.listPaginated(limit: 100)) {
  print(user.id);
}

final invoice = await acme.invoices.create(customerId: 'cus_123', amountCents: 4900);
Free functions
import 'package:acme_api/acme_api.dart';

SdkConfig.configure(ClientConfig(
  baseUrl: 'https://api.acme.com/v1',
  auth: ClientAuthConfig(bearer: token),
));

await for (final user in listUsersPaginated(limit: 100)) {
  print(user.id);
}

The package at a glance

What a developer checks before adding the Dart package to a project.

Runs on
Dart 3; Flutter on iOS, Android, macOS, Windows and Linux
Transport
dart:io HttpClient
Runtime dependencies
None
Models
Immutable classes with fromJson, no build_runner step
Pagination
Lazy Stream, consumed with await for
Ships to
pub.dev, for dart pub and flutter pub

When a call fails

Each common status is its own subclass, so on clauses read like the cases a screen has to handle. A declared error model is decoded onto e.data for an is check.

Typed errors
try {
  await acme.invoices.create(customerId: 'cus_123', amountCents: 4900);
} on SdkRateLimitError {
  // 429 after the built-in retries. Show a retry button.
} on SdkHttpError catch (e) {
  print('acme failed: ${e.statusCode} request ${e.requestId}');
}

What you decide for Dart

Dart has no engine or data-model setting. Models are plain Dart, so your users never run code generation of their own.

  • Client style

    class-namespaced (default), class, namespaced, functions

    A client object with a field per resource, easy to hand to a Provider or a Riverpod provider. Functions are top-level and read a global SdkConfig.

  • Argument style

    positional (default), object

    Named parameters, required where your spec says required, which is the Dart idiom. Object collects them into a request class.

  • Doc comments

    full (default), minimal

    Full writes dartdoc, which feeds the documentation share of your pub points score. Keep it on full.

Method naming and namespacing work the same in every language: operations are grouped by your spec’s tags or by path, and the namespace word is dropped from the method name unless you ask to keep the operationId whole.

pub.dev names are lowercase, underscored and permanent

Package names on pub.dev are global, use underscores (hyphens are not allowed) and stay yours for good. A version can be retracted within seven days, which discourages new resolution but does not free the number. Choose the name and check the first version on a device before you publish.

pub.dev package name
acme_api
  • Not possible: Put a hyphen in the name
  • Not possible: Reuse a version number
  • Still possible: Retract a version within seven days

The same in every generated client

The parts nobody wants to hand-write, and the parts a hand-written client usually skips.

  • Retries with exponential backoff and jitter, on the statuses that mean "try again", with a per-attempt timeout.
  • Idempotency keys on the methods that need them, so a retried write does not become two.
  • Auth wired in from your spec’s security scheme, read from wherever your users keep secrets.
  • Typed errors split by cause: a failed constraint, an HTTP status, a network failure and a timeout are four different things.
  • Pagination that follows whichever contract your spec declares, cursor, offset, page number or next URL, without prefetching the collection.
  • Server-sent events as a native stream, for the operations that stream.
  • Error reporting to Octri’s monitoring, pre-wired and switched off until a consumer opts in.
  1. Point it at your spec

    Upload an OpenAPI or Swagger document, paste a URL, or connect the repository it lives in so a merge to main updates it.

  2. Read the audit

    The spec is scored out of 10 on what a generator can do with it, with every missing schema and colliding operationId named. 9 of the fourteen rules carry a button that writes the fix.

  3. Shape the client

    Client style, argument style, namespacing, auth, retries and pagination, set once for the project and overridden per language or per endpoint.

  4. Generate and read it

    The package is generated, compiled and handed to you as source you can read before anyone installs it.

  5. Publish, then keep publishing

    Release to the registry under your own account. Every later spec change regenerates the client, and the version number comes from your own document.

Questions

Not today. The transport is dart:io’s HttpClient, which Flutter supports on iOS, Android, macOS, Windows and Linux, and on Dart servers and CLIs, but which browsers do not provide.