wordpress_client 10.0.0
wordpress_client: ^10.0.0 copied to clipboard
A library to interact with the Wordpress REST API. Supports most of the common endpoints and all of the CRUD operations on the endpoints.
WordPress Client
A strongly typed WordPress client for Dart & Flutter: REST, GraphQL and the Abilities API.
Features #
- Typed REST client for 30+ endpoints (posts, pages, media, users, comments, taxonomies, blocks, templates, navigation, widgets, settings, themes, ...) with full CRUD. See Supported REST Methods.
- Typed results:
WordpressResponse<T>is a success or a failure; failures classify into a sealedWordpressFailure. - Auth: Application Passwords, JWT (two plugins), or your own
IAuthorization. Credentials never follow the client to another origin. - Resilience: opt-in retries with backoff and
Retry-After. - Caching: GET cache with per-resource invalidation and ETag revalidation. Custom stores supported.
- Pagination:
listPages/listAll, plus parallel page fetching. - GraphQL (WPGraphQL): queries, mutations, partial results, GET, pagination, batching, persisted queries.
- Abilities API (WordPress 6.9+).
- Extensibility: middlewares, Dio interceptors, custom endpoints, raw requests, polling streams.
- Testable:
MockWordpressAdapterserves canned responses, no live site needed.
Install #
dependencies:
wordpress_client: ^10.0.0
Requires Dart >=3.0.0.
Quick start #
import 'package:wordpress_client/wordpress_client.dart';
// From the site root (resolves <site>/wp-json/wp/v2)
final client = WordpressClient.forSite(
siteUrl: Uri.parse('https://example.com'),
);
// Or from the REST base URL
final same = WordpressClient(
baseUrl: Uri.parse('https://example.com/wp-json/wp/v2'),
);
final response = await client.posts.list(ListPostRequest(perPage: 10));
switch (response) {
case WordpressSuccessResponse():
print(response.data.map((p) => p.title?.rendered)); // List<Post>
case WordpressFailureResponse():
print('${response.code}: ${response.message}');
}
Create the client once and reuse it. Call client.dispose() when done.
Configure #
final client = WordpressClient.forSite(
siteUrl: Uri.parse('https://example.com'),
bootstrapper: (b) => b
.withDefaultAuthorization(
WordpressAuth.appPassword(user: 'admin', appPassword: 'xxxx xxxx xxxx'),
)
.withRequestTimeout(const Duration(seconds: 20))
.withRetryPolicy(const RetryPolicy.standard())
.withCache(ttl: const Duration(minutes: 5))
.withDefaultUserAgent('my-app/1.0')
.build(),
);
More: Bootstrap and Configuration, Authorization.
Read, create, update, delete #
final post = (await client.posts.retrieve(RetrievePostRequest(id: 1))).dataOrThrow();
await client.posts.create(CreatePostRequest(
title: 'Hello',
content: '<p>First post</p>',
status: 'draft',
));
await client.posts.update(UpdatePostRequest(id: 1, title: 'Renamed'));
await client.posts.delete(DeletePostRequest(id: 1, force: true));
Write requests need credentials (withDefaultAuthorization, or authorization: on the request).
Handle results #
final r = await client.posts.list(ListPostRequest());
final posts = r.dataOrThrow(); // throws on failure
final safe = r.dataOrNull(); // null on failure
final count = r.map(onSuccess: (s) => s.data.length, onFailure: (_) => 0);
if (r case WordpressFailureResponse(:final failure)) {
switch (failure) {
case NetworkFailure(:final kind): print('network: $kind'); // timeout, connection, cancelled
case AuthFailure(:final reason): print('auth: $reason');
case ApiFailure(:final statusCode, :final error): print('$statusCode ${error?.code}');
case DecodeFailure(): print('bad response body');
case MiddlewareFailure(): print('a middleware aborted it');
case ClientFailure(:final type): print('client: $type');
}
}
More: Error Handling and Responses.
Query fluently #
final res = await client.posts.query
.withSearch('flutter')
.withCategories([3, 7])
.withOrder(Order.desc)
.withPerPage(20)
.execute();
final bySlug = await client.pages.extensions.findBySlug('about'); // Page?
More: Fluent Queries, Interfaces and Extensions.
Paginate #
// Everything in one response (use maxItems to cap it)
final all = await client.posts.listAll(ListPostRequest(), maxItems: 1000);
// Lazily, one response per page
await for (final page in client.posts.listPages(ListPostRequest(), perPage: 50)) {
// page is a WordpressResponse<List<Post>>; a failing page ends the stream
}
// Pages in parallel
final merged = await client.parallel
.list<Post, ListPostRequest>(
interface: client.posts,
requestBuilder: () => [
for (var p = 1; p <= 10; p++)
ParallelRequest(page: p, request: ListPostRequest(page: p, perPage: 50)),
],
)
.merge();
More: Pagination and Finders, Parallel Requests.
Retries #
Off by default. Retries timeouts, connection errors and 429/502/503/504, only for idempotent methods (never POST/PATCH).
b.withRetryPolicy(const RetryPolicy(maxAttempts: 4, baseDelay: Duration(seconds: 1)))
// override for one raw request
client.raw(WordpressRequest(
method: HttpMethod.get,
url: RequestUrl.relative('posts'),
retryPolicy: RetryPolicy.none,
));
More: Retries.
Cache #
b.withCache(
ttl: const Duration(minutes: 5),
ttlFor: (url) => url.path.endsWith('/categories') ? const Duration(hours: 1) : null,
)
GET responses are cached per URL, query and credential. A write evicts only the resource it touched. Expired entries with an ETag are revalidated with a 304. More: Caching.
GraphQL (WPGraphQL) #
final res = await client.graphql.query<List<Map<String, dynamic>>>(
document: r'query($n: Int!) { posts(first: $n) { nodes { id title } } }',
variables: {'n': 5},
parseData: (d) => (d['posts']['nodes'] as List).cast<Map<String, dynamic>>(),
);
// Keep data and errors together (partial results)
final result = await client.graphql.execute<Map<String, dynamic>>(
document: '{ viewer { name } }',
parseData: (d) => d,
requireAuth: true,
);
if (result case WordpressSuccessResponse(:final data)) {
print(data.data); print(data.errors);
}
Also mutate, useGet, paginate / queryAll, batch, persisted queries and registered queries. More: GraphQL.
Abilities API (WordPress 6.9+) #
final abilities = (await client.abilities.list(category: 'site')).dataOrThrow();
final out = await client.abilities.run('my-plugin/get-user-info', input: {'user_id': 1});
Requires an authenticated user. More: Abilities.
Middlewares #
client.registerMiddleware(
DelegatedMiddleware(
onRequestDelegate: (req) async {
print('-> ${req.method.name} ${req.extra[WordpressRequest.metaUrl]}');
return req;
},
onResponseDelegate: (res) async {
print('<- ${res.code} in ${res.duration.inMilliseconds}ms');
return res;
},
),
);
More: Middlewares.
Custom endpoints and raw requests #
// Any URL on the client's origin, untyped
final raw = await client.raw(WordpressRequest(
method: HttpMethod.get,
url: RequestUrl.relative('posts'),
queryParameters: {'per_page': 1},
));
print(raw.code); print(raw.data);
Typed custom endpoints: Using Custom Requests. Polling streams: Streaming and Polling.
Test your code #
import 'package:dio/dio.dart';
import 'package:wordpress_client/testing.dart';
import 'package:wordpress_client/wordpress_client.dart';
final adapter = MockWordpressAdapter()
..reply('GET', '/posts', MockResponse.paged([{'id': 1}], total: 1, totalPages: 1))
..reply('POST', '/posts', MockResponse.wpError(statusCode: 403, code: 'rest_cannot_create'));
final client = WordpressClient.fromDioInstance(
baseUrl: Uri.parse('https://example.com/wp-json/wp/v2'),
instance: Dio()..httpClientAdapter = adapter,
);
expect(adapter.callCount(method: 'GET', path: '/posts'), 1);
More: Testing.
Security notes #
- A request is only sent to the client's own origin (scheme, host and port). Anything else fails with
RequestErrorType.originMismatch. reconfigure(baseUri: ...)to a different origin drops the default credentials and discovery data. Pass new credentials in the same call.- Cache keys hold a SHA-256 digest of the
Authorizationheader, never the header itself.
Documentation #
Everything is in the Wiki:
Contributing #
Bugs and feature requests go to the issue tracker. PRs are welcome.
