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.2dev_dependencies:
build_runner: anyImplement JsonType<T> and annotate with @AutoFactory:
import'package:valync/valync.dart';
@AutoFactory()
classUserimplementsJsonType<User> {
finalString id;
finalString name;
User({requiredthis.id, requiredthis.name});
@overrideUserfromJson(dynamic json) =>User(
id: json['id'] asString,
name: json['name'] asString,
);
}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';
voidmain() {
registerAllFactories();
runApp(constMyApp());
}Built-in types (
EmptyResponse,ListResponse,JsonObjectResponse) are pre-registered automatically — no annotation or setup needed.
final result =awaitvalync<User>('https://api.example.com/users/1');
switch (result) {
caseOk(:final value):print(value.name);
caseErr(: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') {
awaitrefreshToken();
returntrue; // true = retry the request once
}
returnfalse;
},
),
);
// GETfinal result =awaitclient<User>('https://api.example.com/me');
// POST with JSON bodyfinal result =awaitclient<User>(
'https://api.example.com/users',
method:HttpMethod.post,
body: {'name':'Alice'},
);
// POST with multipart filesfinal result =awaitclient<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 =awaitclient<ListResponse>('https://api.example.com/users');
switch (result) {
caseOk(:final value):final users = value.apply((e) =>User().fromJson(e));
print(users.map((u) => u.name).join(', '));
caseErr(:final error):print(error);
}Use EmptyResponse for endpoints that return 204 No Content or a bare success:
final result =awaitclient<EmptyResponse>(
'https://api.example.com/logout',
method:HttpMethod.post,
);
switch (result) {
caseOk():print('logged out');
caseErr(:final error):print(error);
}Use JsonObjectResponse when you need to map data yourself:
final result =awaitclient<JsonObjectResponse>('https://api.example.com/config');
switch (result) {
caseOk(:final value):final config = value.apply((e) =>AppConfig.fromJson(e));
print(config.featureFlags);
caseErr(: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.
ValyncClientcreateClient({
Map<String, String>? headers,
ValyncClientConfig config =constValyncClientConfig(),
})| 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