rest_route 0.0.2
rest_route: ^0.0.2 copied to clipboard
Manage API routes in Flutter efficiently, ensuring type safety, cleaner code, and easier maintenance for RESTful APIs
Introduction #
A simple way to manage API routes in your app with type safety and proper inheritance.
What It Does #
RestRoute<T> is a base class that helps you create API routes flexibly, allowing you to concatenate paths and add parameters easily. NestedRoute<P, T> is used to create child routes, such as "stories/13/comments/5", while maintaining type safety.
When working with APIs, we often need to construct various URL paths dynamically. Managing these routes manually can lead to errors, inconsistencies, and difficult-to-maintain code.
This library helps you build API URLs in a structured, type-safe way. Instead of manually writing strings like "users/123/comments", you can use method chains that are checked by the compiler.
Key Improvements in the New Version #
The updated implementation addresses several key issues:
- Proper Type Safety: Routes now correctly return their concrete types through a
copyWithImplmethod that each subclass must implement - More Flexible Inheritance:
NestedRoutenow accepts anyBaseRouteas a parent, not justRestRoute - Improved Mixin Support: The
RestfulMixinnow works with anyBaseRoute, making it more versatile
Basic Usage #
Installation #
flutter pub add rest_route
This command will add a line to your package's pubspec.yaml file and run an implicit flutter pub get. The added line will look like this:
dependencies:
rest_route: ^0.0.1
Creating a Simple Route #
// Define your route class
class UserRoute extends RestRoute<UserRoute> {
UserRoute([String routePath = '']) : super('users', routePath);
// Required implementation
@override
UserRoute copyWithImpl(String newPath) {
return UserRoute(newPath);
}
// Add custom endpoints as needed
String get profile => join('profile');
}
// Create and use your route
final userRoute = UserRoute();
final profileUrl = userRoute.profile; // Result: "users/profile"
final userWithIdUrl = userRoute.id(123); // Result: "users/123"
The copyWithImpl Method #
In the new implementation, every subclass of RestRoute and NestedRoute must implement a copyWithImpl method. This ensures proper type safety when creating new instances with different paths:
@override
UserRoute copyWithImpl(String newPath) {
return UserRoute(newPath);
}
This method replaces the previous internal _copyWith method and fixes the casting issue in the original implementation.
Main Features #
- Adding IDs:
route.id(123)gives"resource/123" - Joining paths:
route.join("something")gives"resource/something" - Adding query parameters:
route.withQueryParams({"sort": "asc"})gives"resource?sort=asc"
Working with IDs #
There are multiple ways to add an ID:
// These all do the same thing:
userRoute.withId(10).path; // "users/10"
userRoute(10).path; // "users/10"
// Short-hand syntax, which doesn't require calling .path
userRoute.id(10); // "users/10"
// Get just the base path without the ID
userRoute(10).basePath; // "users"
Adding Query Parameters #
// Helper method example
String getUsersFiltered({String? role, int? page}) {
final params = <String, dynamic>{};
if (role != null) params['role'] = role;
if (page != null) params['page'] = page;
return userRoute.withQueryParams(params); // Result: "users?role=admin&page=1"
}
Nested Routes #
For URLs with multiple segments like "users/13/posts/5/comments":
// Parent route
class UserRoute extends RestRoute<UserRoute> {
UserRoute([String routePath = '']) : super('users', routePath);
// Define nested routes
late final posts = PostsRoute(this);
@override
UserRoute copyWithImpl(String newPath) {
return UserRoute(newPath);
}
}
// Define the nested route
class PostsRoute extends NestedRoute<UserRoute, PostsRoute> {
PostsRoute(this.parent, [String routePath = ''])
: super(parent, 'posts', routePath);
final UserRoute parent;
// Define deeper nested routes
late final comments = CommentsRoute(this);
@override
PostsRoute copyWithImpl(String newPath) {
return PostsRoute(parent, newPath);
}
}
class CommentsRoute extends NestedRoute<PostsRoute, CommentsRoute> {
CommentsRoute(this.parent, [String routePath = ''])
: super(parent, 'comments', routePath);
final PostsRoute parent;
String get featured => join('featured');
@override
CommentsRoute copyWithImpl(String newPath) {
return CommentsRoute(parent, newPath);
}
}
// Using the nested routes
final users = UserRoute();
// Create nested paths
users(13).path // "users/13"
users(13).posts.path // "users/13/posts"
users(13).posts(5).path // "users/13/posts/5"
users(13).posts(5).comments.path // "users/13/posts/5/comments"
users(13).posts(5).comments.featured // "users/13/posts/5/comments/featured"
Standard REST Methods #
Add standard CRUD operations with the RestfulMixin:
class UserRoute extends RestRoute<UserRoute> with RestfulMixin {
UserRoute([String routePath = '']) : super('users', routePath);
@override
UserRoute copyWithImpl(String newPath) {
return UserRoute(newPath);
}
}
final users = UserRoute();
users.list; // "users" (GET - for listing all)
users.create; // "users" (POST - for creating new)
users.get(123); // "users/123" (GET - for getting one)
users.update(123); // "users/123" (PUT - for updating)
users.delete(123); // "users/123" (DELETE - for removing)
RestfulMixin for All Routes #
The updated RestfulMixin can be applied to any class that extends BaseRoute, making it more versatile:
// Works with both RestRoute and NestedRoute
mixin RestfulMixin on BaseRoute {
// RESTful API methods
String get(dynamic id) => BaseRoute.joinPaths(path, id.toString());
String get create => basePath;
String get list => basePath;
String update(dynamic id) => BaseRoute.joinPaths(path, id.toString());
String delete(dynamic id) => BaseRoute.joinPaths(path, id.toString());
}
Complete Example #
// Define all your routes in one place
abstract class ApiRoutes {
static final users = UserRoute();
}
// Multi-level nested route example
class UserRoute extends RestRoute<UserRoute> with RestfulMixin {
UserRoute([String routePath = '']) : super('users', routePath);
late final posts = PostsRoute(this);
late final settings = SimpleNestedRoute(this, 'settings');
@override
UserRoute copyWithImpl(String newPath) {
return UserRoute(newPath);
}
}
class PostsRoute extends NestedRoute<UserRoute, PostsRoute> with RestfulMixin {
PostsRoute(this.parent, [String routePath = ''])
: super(parent, 'posts', routePath);
final UserRoute parent;
late final comments = CommentsRoute(this);
@override
PostsRoute copyWithImpl(String newPath) {
return PostsRoute(parent, newPath);
}
}
class CommentsRoute extends NestedRoute<PostsRoute, CommentsRoute>
with RestfulMixin {
CommentsRoute(this.parent, [String routePath = ''])
: super(parent, 'comments', routePath);
final PostsRoute parent;
String get featured => join('featured');
String get report => join('report');
@override
CommentsRoute copyWithImpl(String newPath) {
return CommentsRoute(parent, newPath);
}
}
// Using in your code
void apiCalls() async {
final dio = Dio(); // Your HTTP client
// User endpoints with RestfulMixin
await dio.get(ApiRoutes.users.list); // "users"
await dio.post(ApiRoutes.users.create); // "users"
await dio.get(ApiRoutes.users.get(123)); // "users/123"
// Nested routes
await dio.get(ApiRoutes.users(123).posts.list); // "users/123/posts"
await dio.get(ApiRoutes.users(123).posts(5).comments.featured); // "users/123/posts/5/comments/featured"
// Using SimpleNestedRoute
await dio.get(ApiRoutes.users(123).settings.path); // "users/123/settings"
// With query parameters
await dio.get(ApiRoutes.users.withQueryParams({
"role": "admin",
"page": 1
})); // "users?role=admin&page=1"
}
Enhanced Types and Flexibility #
SimpleRoute and SimpleNestedRoute #
The library provides ready-to-use implementations for simpler use cases:
// Create a simple route without defining a class
final simple = SimpleRoute('products');
simple.path; // "products"
simple(5).path; // "products/5"
// Create a simple nested route
final user = UserRoute(1);
final settings = SimpleNestedRoute(user, 'settings');
settings.path; // "users/1/settings"
settings('profile').path; // "users/1/settings/profile"
Both implement the required copyWithImpl method, so you don't need to implement it yourself.
Testing #
The library supports easy testing of your routes:
test('should handle deep nesting correctly', () {
final users = UserRoute();
// Basic path tests
expect(users.path, 'users');
expect(users.posts.path, 'users/posts');
expect(users.posts.comments.path, 'users/posts/comments');
// With IDs
expect(users(1).path, 'users/1');
expect(users(1).posts.path, 'users/1/posts');
expect(users(1).posts(2).path, 'users/1/posts/2');
expect(users(1).posts(2).comments.path, 'users/1/posts/2/comments');
expect(users(1).posts(2).comments(3).path, 'users/1/posts/2/comments/3');
// Custom endpoints
expect(
users(1).posts(2).comments.featured,
'users/1/posts/2/comments/featured',
);
});
Tips for Good Implementation #
- Always implement
copyWithImpl: This is required for type safety - Store the parent in nested routes: This makes it easier to create the
copyWithImplmethod - Use
late finalfor nested routes: This prevents recreating them every time they're accessed - Group related endpoints by resource: Keep your API routes organized
- Use
RestfulMixinfor standard CRUD operations: Minimize boilerplate code - Create a centralized place for all routes: Like the
ApiRoutesclass in the example