A Flutter package for making typed HTTP requests with automatic JSON deserialization, structured success/failure responses, and built-in error handling.
- Typed HTTP client — responses are deserialized directly into your Dart models
Result<T, ApiError>return type (viaoption_result) — no uncaught exceptions- Structured API responses with
success/failedstatus discrimination - Multipart file upload support
- Built-in generic response types:
EmptyResponse,ListResponse,JsonObjectResponse - Auto-generated factory registry via the
@AutoFactoryannotation andbuild_runner - Optional
createClientfor shared headers and automatic retry on error
Add to your pubspec.yaml:
dependencies:
valync: ^0.1.2
dev_dependencies:
build_runner: anyImplement JsonType<T> and annotate with @AutoFactory:
import 'package:valync/valync.dart';
@AutoFactory()
class User implements JsonType<User> {
final String id;
final String name;
User({required this.id, required this.name});
@override
User fromJson(dynamic json) => User(
id: json['id'] as String,
name: json['name'] as String,
);
}flutter pub run build_runner buildThis generates lib/type_factories.dart containing a registerAllFactories() function.
Call registerAllFactories() before making any requests — typically in main():
import 'package:your_app/type_factories.dart';
void main() {
registerAllFactories();
runApp(const MyApp());
}Built-in types (
EmptyResponse,ListResponse,JsonObjectResponse) are pre-registered automatically — no annotation or setup needed.
final result = await valync<User>('https://api.example.com/users/1');
switch (result) {
case Ok(:final value):
print(value.name);
case Err(:final error):
print('${error.name}: ${error.message}');
}final client = createClient(
headers: {'X-App-Version': '1.0.0'},
config: ValyncClientConfig(
headers: () => {'Authorization': 'Bearer $token'},
onError: (error) async {
if (error.code.unwrapOr('') == '401') {
await refreshToken();
return true; // true = retry the request once
}
return false;
},
),
);
// GET
final result = await client<User>('https://api.example.com/me');
// POST with JSON body
final result = await client<User>(
'https://api.example.com/users',
method: HttpMethod.post,
body: {'name': 'Alice'},
);
// POST with multipart files
final result = await client<User>(
'https://api.example.com/avatar',
method: HttpMethod.post,
files: [await http.MultipartFile.fromPath('avatar', '/path/to/file.jpg')],
);Use ListResponse when the API returns an array as data:
final result = await client<ListResponse>('https://api.example.com/users');
switch (result) {
case Ok(:final value):
final users = value.apply((e) => User().fromJson(e));
print(users.map((u) => u.name).join(', '));
case Err(:final error):
print(error);
}Use EmptyResponse for endpoints that return 204 No Content or a bare success:
final result = await client<EmptyResponse>(
'https://api.example.com/logout',
method: HttpMethod.post,
);
switch (result) {
case Ok():
print('logged out');
case Err(:final error):
print(error);
}Use JsonObjectResponse when you need to map data yourself:
final result = await client<JsonObjectResponse>('https://api.example.com/config');
switch (result) {
case Ok(:final value):
final config = value.apply((e) => AppConfig.fromJson(e));
print(config.featureFlags);
case Err(:final error):
print(error);
}HttpMethod.get, HttpMethod.post, HttpMethod.put, HttpMethod.patch, HttpMethod.delete
Stateless one-off request.
Future<Result<T, ApiError>> valync<T>(
String url, {
HttpMethod method = HttpMethod.get,
Map<String, dynamic>? body,
Map<String, String>? headers,
List<http.MultipartFile>? files,
})Returns a ValyncClient with shared configuration.
ValyncClient createClient({
Map<String, String>? headers,
ValyncClientConfig config = const ValyncClientConfig(),
})| Field | Type | Description |
|---|---|---|
headers |
Map<String, String> Function()? |
Dynamic headers merged into every request (e.g. auth tokens) |
onError |
Future<bool> Function(ApiError)? |
Called on error — return true to retry the request once |
| Field | Type | Description |
|---|---|---|
name |
String |
Machine-readable error identifier |
message |
String |
Human-readable description |
code |
Option<String> |
Optional error code |
Built-in error names: ServerUnreacheable, InternalServerError, UnknownError.
| Type | Use when data is |
|---|---|
EmptyResponse |
absent or irrelevant (e.g. 204) |
ListResponse |
a JSON array — use .apply((e) => T.fromJson(e)) to map |
JsonObjectResponse |
an arbitrary value — use .apply((e) => T.fromJson(e)) to map |
Register a factory manually without code generation:
registerFactory(User, User(id: '', name: ''));The package expects responses in this shape:
{ "status": "success", "data": { ... } }
{ "status": "failed", "error": { "name": "...", "message": "...", "code": "..." } }204 No Content responses are treated as success with no data.
A runnable Flutter example app is available in examples/basic.
It demonstrates one-off requests, list responses, empty responses, error handling,
and createClient — all using a mock HTTP client, no server required.
- Issues and contributions: open a GitHub issue or pull request
- Built on
option_result,http, andsource_gen