moicle 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -0
- package/assets/agents/developers/flutter-mobile-dev.md +69 -0
- package/assets/agents/developers/go-backend-dev.md +57 -0
- package/assets/agents/developers/laravel-backend-dev.md +123 -0
- package/assets/agents/developers/react-frontend-dev.md +69 -0
- package/assets/agents/developers/remix-fullstack-dev.md +69 -0
- package/assets/agents/utilities/api-designer.md +76 -0
- package/assets/agents/utilities/clean-architect.md +83 -0
- package/assets/agents/utilities/code-reviewer.md +76 -0
- package/assets/agents/utilities/db-designer.md +68 -0
- package/assets/agents/utilities/devops.md +71 -0
- package/assets/agents/utilities/docs-writer.md +75 -0
- package/assets/agents/utilities/perf-optimizer.md +87 -0
- package/assets/agents/utilities/refactor.md +173 -0
- package/assets/agents/utilities/security-audit.md +203 -0
- package/assets/agents/utilities/test-writer.md +139 -0
- package/assets/architecture/clean-architecture.md +143 -0
- package/assets/architecture/flutter-mobile.md +304 -0
- package/assets/architecture/go-backend.md +217 -0
- package/assets/architecture/laravel-backend.md +303 -0
- package/assets/architecture/monorepo.md +162 -0
- package/assets/architecture/react-frontend.md +268 -0
- package/assets/architecture/remix-fullstack.md +272 -0
- package/assets/commands/bootstrap.md +98 -0
- package/assets/commands/brainstorm.md +440 -0
- package/assets/skills/feature-workflow/SKILL.md +298 -0
- package/assets/skills/hotfix-workflow/SKILL.md +368 -0
- package/assets/templates/flutter/CLAUDE.md +454 -0
- package/assets/templates/go-gin/CLAUDE.md +244 -0
- package/assets/templates/monorepo/CLAUDE.md +362 -0
- package/assets/templates/react-vite/CLAUDE.md +304 -0
- package/assets/templates/remix/CLAUDE.md +304 -0
- package/bin/cli.js +76 -0
- package/dist/commands/disable.d.ts +3 -0
- package/dist/commands/disable.d.ts.map +1 -0
- package/dist/commands/disable.js +188 -0
- package/dist/commands/disable.js.map +1 -0
- package/dist/commands/enable.d.ts +3 -0
- package/dist/commands/enable.d.ts.map +1 -0
- package/dist/commands/enable.js +191 -0
- package/dist/commands/enable.js.map +1 -0
- package/dist/commands/install.d.ts +3 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +290 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +75 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/postinstall.d.ts +2 -0
- package/dist/commands/postinstall.d.ts.map +1 -0
- package/dist/commands/postinstall.js +25 -0
- package/dist/commands/postinstall.js.map +1 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +118 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/uninstall.d.ts +3 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +178 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/config.d.ts +13 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +95 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/symlink.d.ts +24 -0
- package/dist/utils/symlink.d.ts.map +1 -0
- package/dist/utils/symlink.js +313 -0
- package/dist/utils/symlink.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
# CLAUDE.md - Flutter Mobile App Template
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
Cross-platform mobile application built with:
|
|
6
|
+
- **Flutter 3.x** - UI toolkit
|
|
7
|
+
- **Dart** - Programming language
|
|
8
|
+
- **Riverpod** - State management
|
|
9
|
+
- **GoRouter** - Navigation
|
|
10
|
+
- **Dio** - HTTP client
|
|
11
|
+
- **Freezed** - Code generation for immutable classes
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Install dependencies
|
|
17
|
+
flutter pub get
|
|
18
|
+
|
|
19
|
+
# Generate code (freezed, json_serializable)
|
|
20
|
+
dart run build_runner build --delete-conflicting-outputs
|
|
21
|
+
|
|
22
|
+
# Run development
|
|
23
|
+
flutter run
|
|
24
|
+
|
|
25
|
+
# Run on specific device
|
|
26
|
+
flutter run -d chrome
|
|
27
|
+
flutter run -d ios
|
|
28
|
+
flutter run -d android
|
|
29
|
+
|
|
30
|
+
# Build release
|
|
31
|
+
flutter build apk
|
|
32
|
+
flutter build ios
|
|
33
|
+
flutter build web
|
|
34
|
+
|
|
35
|
+
# Run tests
|
|
36
|
+
flutter test
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Project Structure
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
{project_name}/
|
|
43
|
+
├── lib/
|
|
44
|
+
│ ├── core/ # Core utilities
|
|
45
|
+
│ │ ├── config/
|
|
46
|
+
│ │ │ └── app_config.dart
|
|
47
|
+
│ │ ├── constants/
|
|
48
|
+
│ │ │ └── api_constants.dart
|
|
49
|
+
│ │ ├── network/
|
|
50
|
+
│ │ │ ├── api_client.dart
|
|
51
|
+
│ │ │ └── api_exceptions.dart
|
|
52
|
+
│ │ ├── router/
|
|
53
|
+
│ │ │ └── app_router.dart
|
|
54
|
+
│ │ └── theme/
|
|
55
|
+
│ │ └── app_theme.dart
|
|
56
|
+
│ ├── features/ # Feature modules
|
|
57
|
+
│ │ └── {feature}/
|
|
58
|
+
│ │ ├── data/
|
|
59
|
+
│ │ │ ├── models/
|
|
60
|
+
│ │ │ ├── repositories/
|
|
61
|
+
│ │ │ └── datasources/
|
|
62
|
+
│ │ ├── domain/
|
|
63
|
+
│ │ │ └── entities/
|
|
64
|
+
│ │ ├── presentation/
|
|
65
|
+
│ │ │ ├── providers/
|
|
66
|
+
│ │ │ ├── screens/
|
|
67
|
+
│ │ │ └── widgets/
|
|
68
|
+
│ │ └── {feature}.dart # Barrel file
|
|
69
|
+
│ ├── shared/ # Shared components
|
|
70
|
+
│ │ ├── widgets/
|
|
71
|
+
│ │ └── utils/
|
|
72
|
+
│ └── main.dart
|
|
73
|
+
├── test/
|
|
74
|
+
├── android/
|
|
75
|
+
├── ios/
|
|
76
|
+
├── web/
|
|
77
|
+
├── pubspec.yaml
|
|
78
|
+
└── analysis_options.yaml
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Key Patterns and Conventions
|
|
82
|
+
|
|
83
|
+
### File Naming
|
|
84
|
+
- Use `snake_case.dart` for all Dart files
|
|
85
|
+
- Feature barrel files: `{feature}.dart`
|
|
86
|
+
- Widgets: `{name}_widget.dart`
|
|
87
|
+
- Screens: `{name}_screen.dart`
|
|
88
|
+
- Providers: `{name}_provider.dart`
|
|
89
|
+
|
|
90
|
+
### Model Pattern (Freezed)
|
|
91
|
+
|
|
92
|
+
```dart
|
|
93
|
+
// features/users/data/models/user_model.dart
|
|
94
|
+
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
95
|
+
|
|
96
|
+
part 'user_model.freezed.dart';
|
|
97
|
+
part 'user_model.g.dart';
|
|
98
|
+
|
|
99
|
+
@freezed
|
|
100
|
+
class UserModel with _$UserModel {
|
|
101
|
+
const factory UserModel({
|
|
102
|
+
required String id,
|
|
103
|
+
required String name,
|
|
104
|
+
required String email,
|
|
105
|
+
@JsonKey(name: 'created_at') DateTime? createdAt,
|
|
106
|
+
}) = _UserModel;
|
|
107
|
+
|
|
108
|
+
factory UserModel.fromJson(Map<String, dynamic> json) =>
|
|
109
|
+
_$UserModelFromJson(json);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@freezed
|
|
113
|
+
class UserListResponse with _$UserListResponse {
|
|
114
|
+
const factory UserListResponse({
|
|
115
|
+
required List<UserModel> data,
|
|
116
|
+
required int total,
|
|
117
|
+
required int page,
|
|
118
|
+
required int limit,
|
|
119
|
+
@JsonKey(name: 'total_pages') required int totalPages,
|
|
120
|
+
}) = _UserListResponse;
|
|
121
|
+
|
|
122
|
+
factory UserListResponse.fromJson(Map<String, dynamic> json) =>
|
|
123
|
+
_$UserListResponseFromJson(json);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Repository Pattern
|
|
128
|
+
|
|
129
|
+
```dart
|
|
130
|
+
// features/users/data/repositories/user_repository.dart
|
|
131
|
+
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
132
|
+
|
|
133
|
+
part 'user_repository.g.dart';
|
|
134
|
+
|
|
135
|
+
@riverpod
|
|
136
|
+
UserRepository userRepository(UserRepositoryRef ref) {
|
|
137
|
+
return UserRepository(ref.watch(apiClientProvider));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
class UserRepository {
|
|
141
|
+
final ApiClient _apiClient;
|
|
142
|
+
|
|
143
|
+
UserRepository(this._apiClient);
|
|
144
|
+
|
|
145
|
+
Future<UserListResponse> getUsers({
|
|
146
|
+
int page = 1,
|
|
147
|
+
int limit = 10,
|
|
148
|
+
String? search,
|
|
149
|
+
}) async {
|
|
150
|
+
final response = await _apiClient.get(
|
|
151
|
+
'/users',
|
|
152
|
+
queryParameters: {
|
|
153
|
+
'page': page,
|
|
154
|
+
'limit': limit,
|
|
155
|
+
if (search != null) 'search': search,
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
return UserListResponse.fromJson(response.data);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
Future<UserModel> getUserById(String id) async {
|
|
162
|
+
final response = await _apiClient.get('/users/$id');
|
|
163
|
+
return UserModel.fromJson(response.data);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
Future<UserModel> createUser(CreateUserDto data) async {
|
|
167
|
+
final response = await _apiClient.post('/users', data: data.toJson());
|
|
168
|
+
return UserModel.fromJson(response.data);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Provider Pattern (Riverpod)
|
|
174
|
+
|
|
175
|
+
```dart
|
|
176
|
+
// features/users/presentation/providers/users_provider.dart
|
|
177
|
+
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
178
|
+
|
|
179
|
+
part 'users_provider.g.dart';
|
|
180
|
+
|
|
181
|
+
@riverpod
|
|
182
|
+
class UsersNotifier extends _$UsersNotifier {
|
|
183
|
+
@override
|
|
184
|
+
Future<UserListResponse> build({int page = 1}) async {
|
|
185
|
+
final repository = ref.watch(userRepositoryProvider);
|
|
186
|
+
return repository.getUsers(page: page);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
Future<void> refresh() async {
|
|
190
|
+
state = const AsyncLoading();
|
|
191
|
+
state = await AsyncValue.guard(() => ref.read(userRepositoryProvider).getUsers());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
Future<void> createUser(CreateUserDto data) async {
|
|
195
|
+
await ref.read(userRepositoryProvider).createUser(data);
|
|
196
|
+
ref.invalidateSelf();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Simple state provider
|
|
201
|
+
@riverpod
|
|
202
|
+
class SelectedUser extends _$SelectedUser {
|
|
203
|
+
@override
|
|
204
|
+
UserModel? build() => null;
|
|
205
|
+
|
|
206
|
+
void select(UserModel user) => state = user;
|
|
207
|
+
void clear() => state = null;
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Screen Pattern
|
|
212
|
+
|
|
213
|
+
```dart
|
|
214
|
+
// features/users/presentation/screens/users_screen.dart
|
|
215
|
+
import 'package:flutter/material.dart';
|
|
216
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
217
|
+
|
|
218
|
+
class UsersScreen extends ConsumerWidget {
|
|
219
|
+
const UsersScreen({super.key});
|
|
220
|
+
|
|
221
|
+
@override
|
|
222
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
223
|
+
final usersAsync = ref.watch(usersNotifierProvider());
|
|
224
|
+
|
|
225
|
+
return Scaffold(
|
|
226
|
+
appBar: AppBar(
|
|
227
|
+
title: const Text('Users'),
|
|
228
|
+
actions: [
|
|
229
|
+
IconButton(
|
|
230
|
+
icon: const Icon(Icons.add),
|
|
231
|
+
onPressed: () => context.push('/users/new'),
|
|
232
|
+
),
|
|
233
|
+
],
|
|
234
|
+
),
|
|
235
|
+
body: usersAsync.when(
|
|
236
|
+
loading: () => const Center(child: CircularProgressIndicator()),
|
|
237
|
+
error: (error, stack) => Center(child: Text('Error: $error')),
|
|
238
|
+
data: (response) => RefreshIndicator(
|
|
239
|
+
onRefresh: () => ref.read(usersNotifierProvider().notifier).refresh(),
|
|
240
|
+
child: ListView.builder(
|
|
241
|
+
itemCount: response.data.length,
|
|
242
|
+
itemBuilder: (context, index) {
|
|
243
|
+
final user = response.data[index];
|
|
244
|
+
return UserListTile(
|
|
245
|
+
user: user,
|
|
246
|
+
onTap: () => context.push('/users/${user.id}'),
|
|
247
|
+
);
|
|
248
|
+
},
|
|
249
|
+
),
|
|
250
|
+
),
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Widget Pattern
|
|
258
|
+
|
|
259
|
+
```dart
|
|
260
|
+
// features/users/presentation/widgets/user_list_tile.dart
|
|
261
|
+
import 'package:flutter/material.dart';
|
|
262
|
+
|
|
263
|
+
class UserListTile extends StatelessWidget {
|
|
264
|
+
final UserModel user;
|
|
265
|
+
final VoidCallback? onTap;
|
|
266
|
+
|
|
267
|
+
const UserListTile({
|
|
268
|
+
super.key,
|
|
269
|
+
required this.user,
|
|
270
|
+
this.onTap,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
@override
|
|
274
|
+
Widget build(BuildContext context) {
|
|
275
|
+
return ListTile(
|
|
276
|
+
leading: CircleAvatar(
|
|
277
|
+
child: Text(user.name[0].toUpperCase()),
|
|
278
|
+
),
|
|
279
|
+
title: Text(user.name),
|
|
280
|
+
subtitle: Text(user.email),
|
|
281
|
+
trailing: const Icon(Icons.chevron_right),
|
|
282
|
+
onTap: onTap,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Router Configuration
|
|
289
|
+
|
|
290
|
+
```dart
|
|
291
|
+
// core/router/app_router.dart
|
|
292
|
+
import 'package:go_router/go_router.dart';
|
|
293
|
+
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
294
|
+
|
|
295
|
+
part 'app_router.g.dart';
|
|
296
|
+
|
|
297
|
+
@riverpod
|
|
298
|
+
GoRouter appRouter(AppRouterRef ref) {
|
|
299
|
+
return GoRouter(
|
|
300
|
+
initialLocation: '/',
|
|
301
|
+
routes: [
|
|
302
|
+
GoRoute(
|
|
303
|
+
path: '/',
|
|
304
|
+
builder: (context, state) => const HomeScreen(),
|
|
305
|
+
),
|
|
306
|
+
GoRoute(
|
|
307
|
+
path: '/users',
|
|
308
|
+
builder: (context, state) => const UsersScreen(),
|
|
309
|
+
routes: [
|
|
310
|
+
GoRoute(
|
|
311
|
+
path: 'new',
|
|
312
|
+
builder: (context, state) => const CreateUserScreen(),
|
|
313
|
+
),
|
|
314
|
+
GoRoute(
|
|
315
|
+
path: ':id',
|
|
316
|
+
builder: (context, state) {
|
|
317
|
+
final id = state.pathParameters['id']!;
|
|
318
|
+
return UserDetailScreen(userId: id);
|
|
319
|
+
},
|
|
320
|
+
),
|
|
321
|
+
],
|
|
322
|
+
),
|
|
323
|
+
],
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Adding New Feature
|
|
329
|
+
|
|
330
|
+
1. Create feature directory structure:
|
|
331
|
+
```bash
|
|
332
|
+
mkdir -p lib/features/{feature}/{data/{models,repositories,datasources},domain/entities,presentation/{providers,screens,widgets}}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
2. Create model with Freezed (`data/models/{entity}_model.dart`)
|
|
336
|
+
3. Create repository (`data/repositories/{entity}_repository.dart`)
|
|
337
|
+
4. Create providers (`presentation/providers/{entity}_provider.dart`)
|
|
338
|
+
5. Create screens (`presentation/screens/{entity}_screen.dart`)
|
|
339
|
+
6. Create barrel file (`{feature}.dart`)
|
|
340
|
+
7. Add routes in `app_router.dart`
|
|
341
|
+
8. Run code generation: `dart run build_runner build`
|
|
342
|
+
|
|
343
|
+
## API Client
|
|
344
|
+
|
|
345
|
+
```dart
|
|
346
|
+
// core/network/api_client.dart
|
|
347
|
+
import 'package:dio/dio.dart';
|
|
348
|
+
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
349
|
+
|
|
350
|
+
part 'api_client.g.dart';
|
|
351
|
+
|
|
352
|
+
@riverpod
|
|
353
|
+
ApiClient apiClient(ApiClientRef ref) {
|
|
354
|
+
return ApiClient(baseUrl: AppConfig.apiBaseUrl);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
class ApiClient {
|
|
358
|
+
final Dio _dio;
|
|
359
|
+
|
|
360
|
+
ApiClient({required String baseUrl}) : _dio = Dio(BaseOptions(
|
|
361
|
+
baseUrl: baseUrl,
|
|
362
|
+
connectTimeout: const Duration(seconds: 10),
|
|
363
|
+
receiveTimeout: const Duration(seconds: 10),
|
|
364
|
+
)) {
|
|
365
|
+
_dio.interceptors.add(LogInterceptor());
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
void setAuthToken(String token) {
|
|
369
|
+
_dio.options.headers['Authorization'] = 'Bearer $token';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
Future<Response> get(String path, {Map<String, dynamic>? queryParameters}) {
|
|
373
|
+
return _dio.get(path, queryParameters: queryParameters);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
Future<Response> post(String path, {dynamic data}) {
|
|
377
|
+
return _dio.post(path, data: data);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Configuration
|
|
383
|
+
|
|
384
|
+
### pubspec.yaml
|
|
385
|
+
```yaml
|
|
386
|
+
dependencies:
|
|
387
|
+
flutter:
|
|
388
|
+
sdk: flutter
|
|
389
|
+
flutter_riverpod: ^2.4.0
|
|
390
|
+
riverpod_annotation: ^2.3.0
|
|
391
|
+
go_router: ^13.0.0
|
|
392
|
+
dio: ^5.4.0
|
|
393
|
+
freezed_annotation: ^2.4.0
|
|
394
|
+
json_annotation: ^4.8.0
|
|
395
|
+
|
|
396
|
+
dev_dependencies:
|
|
397
|
+
flutter_test:
|
|
398
|
+
sdk: flutter
|
|
399
|
+
build_runner: ^2.4.0
|
|
400
|
+
riverpod_generator: ^2.3.0
|
|
401
|
+
freezed: ^2.4.0
|
|
402
|
+
json_serializable: ^6.7.0
|
|
403
|
+
flutter_lints: ^3.0.0
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Environment Config
|
|
407
|
+
```dart
|
|
408
|
+
// core/config/app_config.dart
|
|
409
|
+
class AppConfig {
|
|
410
|
+
static const String apiBaseUrl = String.fromEnvironment(
|
|
411
|
+
'API_BASE_URL',
|
|
412
|
+
defaultValue: 'http://localhost:8080/api',
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
static const bool isProduction = bool.fromEnvironment('PRODUCTION');
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Run with environment:
|
|
420
|
+
```bash
|
|
421
|
+
flutter run --dart-define=API_BASE_URL=https://api.example.com
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## Testing
|
|
425
|
+
|
|
426
|
+
```dart
|
|
427
|
+
// test/features/users/users_provider_test.dart
|
|
428
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
429
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
430
|
+
import 'package:mocktail/mocktail.dart';
|
|
431
|
+
|
|
432
|
+
class MockUserRepository extends Mock implements UserRepository {}
|
|
433
|
+
|
|
434
|
+
void main() {
|
|
435
|
+
late MockUserRepository mockRepository;
|
|
436
|
+
|
|
437
|
+
setUp(() {
|
|
438
|
+
mockRepository = MockUserRepository();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test('fetches users successfully', () async {
|
|
442
|
+
when(() => mockRepository.getUsers()).thenAnswer(
|
|
443
|
+
(_) async => UserListResponse(data: [testUser], total: 1, page: 1, limit: 10, totalPages: 1),
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
final container = ProviderContainer(
|
|
447
|
+
overrides: [userRepositoryProvider.overrideWithValue(mockRepository)],
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
final result = await container.read(usersNotifierProvider().future);
|
|
451
|
+
expect(result.data.length, 1);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
```
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# CLAUDE.md - Go + Gin Backend Template
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
Backend API service built with:
|
|
6
|
+
- **Go 1.22+** - Programming language
|
|
7
|
+
- **Gin** - HTTP web framework
|
|
8
|
+
- **GORM** - ORM for database operations
|
|
9
|
+
- **Redis** - Caching and session storage
|
|
10
|
+
- **Viper** - Configuration management
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Install dependencies
|
|
16
|
+
go mod download
|
|
17
|
+
|
|
18
|
+
# Run development server
|
|
19
|
+
go run cmd/api/main.go
|
|
20
|
+
|
|
21
|
+
# Build for production
|
|
22
|
+
go build -o bin/api cmd/api/main.go
|
|
23
|
+
|
|
24
|
+
# Run tests
|
|
25
|
+
go test ./...
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Project Structure
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
{project_name}/
|
|
32
|
+
├── cmd/
|
|
33
|
+
│ └── api/
|
|
34
|
+
│ └── main.go # Application entry point
|
|
35
|
+
├── internal/
|
|
36
|
+
│ ├── config/ # Configuration loading (Viper)
|
|
37
|
+
│ │ └── config.go
|
|
38
|
+
│ ├── middleware/ # HTTP middlewares
|
|
39
|
+
│ │ ├── auth.go
|
|
40
|
+
│ │ ├── cors.go
|
|
41
|
+
│ │ └── logger.go
|
|
42
|
+
│ └── modules/ # Feature modules
|
|
43
|
+
│ ├── router/
|
|
44
|
+
│ │ └── router.go # Route registration
|
|
45
|
+
│ └── {module}/
|
|
46
|
+
│ ├── controllers/
|
|
47
|
+
│ │ └── controller.go
|
|
48
|
+
│ ├── models/
|
|
49
|
+
│ │ └── entity.go
|
|
50
|
+
│ ├── usecases/
|
|
51
|
+
│ │ └── usecase.go
|
|
52
|
+
│ └── init.go
|
|
53
|
+
├── pkg/ # Shared packages
|
|
54
|
+
│ ├── database/
|
|
55
|
+
│ │ └── database.go
|
|
56
|
+
│ ├── queue/
|
|
57
|
+
│ │ └── redis.go
|
|
58
|
+
│ └── response/
|
|
59
|
+
│ └── response.go
|
|
60
|
+
├── config.yaml # Configuration file
|
|
61
|
+
├── go.mod
|
|
62
|
+
└── go.sum
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Key Patterns and Conventions
|
|
66
|
+
|
|
67
|
+
### File Naming
|
|
68
|
+
- Use `snake_case.go` for all Go files
|
|
69
|
+
- One struct per file when possible
|
|
70
|
+
|
|
71
|
+
### Module Structure
|
|
72
|
+
Each module follows the layered architecture:
|
|
73
|
+
- **controllers/**: HTTP handlers, request/response mapping
|
|
74
|
+
- **usecases/**: Business logic
|
|
75
|
+
- **models/**: GORM models and DTOs
|
|
76
|
+
|
|
77
|
+
### Module Init Pattern
|
|
78
|
+
|
|
79
|
+
```go
|
|
80
|
+
// internal/modules/{module}/init.go
|
|
81
|
+
package module
|
|
82
|
+
|
|
83
|
+
func Init(r *gin.Engine, db *gorm.DB) {
|
|
84
|
+
repo := NewRepository(db)
|
|
85
|
+
uc := NewUseCase(repo)
|
|
86
|
+
ctrl := NewController(uc)
|
|
87
|
+
|
|
88
|
+
group := r.Group("/api/{module}")
|
|
89
|
+
{
|
|
90
|
+
group.GET("/", ctrl.List)
|
|
91
|
+
group.GET("/:id", ctrl.Get)
|
|
92
|
+
group.POST("/", ctrl.Create)
|
|
93
|
+
group.PUT("/:id", ctrl.Update)
|
|
94
|
+
group.DELETE("/:id", ctrl.Delete)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Response Format
|
|
100
|
+
|
|
101
|
+
```go
|
|
102
|
+
// Success response
|
|
103
|
+
c.JSON(http.StatusOK, gin.H{
|
|
104
|
+
"data": result,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Error response
|
|
108
|
+
c.JSON(http.StatusBadRequest, gin.H{
|
|
109
|
+
"error": "validation failed",
|
|
110
|
+
"details": errors,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Paginated response
|
|
114
|
+
c.JSON(http.StatusOK, gin.H{
|
|
115
|
+
"data": items,
|
|
116
|
+
"total": total,
|
|
117
|
+
"page": page,
|
|
118
|
+
"limit": limit,
|
|
119
|
+
"total_pages": totalPages,
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### GORM Model Pattern
|
|
124
|
+
|
|
125
|
+
```go
|
|
126
|
+
type Entity struct {
|
|
127
|
+
ID string `gorm:"type:char(36);primaryKey" json:"id"`
|
|
128
|
+
Name string `gorm:"type:varchar(255);not null" json:"name"`
|
|
129
|
+
Status string `gorm:"type:varchar(50);default:'active'" json:"status"`
|
|
130
|
+
CreatedAt time.Time `json:"created_at"`
|
|
131
|
+
UpdatedAt time.Time `json:"updated_at"`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func (e *Entity) BeforeCreate(tx *gorm.DB) error {
|
|
135
|
+
e.ID = uuid.New().String()
|
|
136
|
+
return nil
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Adding New Module
|
|
141
|
+
|
|
142
|
+
1. Create module directory structure:
|
|
143
|
+
```bash
|
|
144
|
+
mkdir -p internal/modules/{module}/{controllers,models,usecases}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
2. Create model (`models/entity.go`):
|
|
148
|
+
```go
|
|
149
|
+
type Entity struct {
|
|
150
|
+
ID string `gorm:"primaryKey" json:"id"`
|
|
151
|
+
Name string `json:"name"`
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
3. Create usecase (`usecases/usecase.go`):
|
|
156
|
+
```go
|
|
157
|
+
type UseCase struct {
|
|
158
|
+
db *gorm.DB
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func (uc *UseCase) List() ([]models.Entity, error) {
|
|
162
|
+
var items []models.Entity
|
|
163
|
+
return items, uc.db.Find(&items).Error
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
4. Create controller (`controllers/controller.go`):
|
|
168
|
+
```go
|
|
169
|
+
type Controller struct {
|
|
170
|
+
uc *usecases.UseCase
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func (ctrl *Controller) List(c *gin.Context) {
|
|
174
|
+
items, err := ctrl.uc.List()
|
|
175
|
+
if err != nil {
|
|
176
|
+
c.JSON(500, gin.H{"error": err.Error()})
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
c.JSON(200, gin.H{"data": items})
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
5. Create init.go and register in router
|
|
184
|
+
|
|
185
|
+
## API Endpoints Pattern
|
|
186
|
+
|
|
187
|
+
| Method | Path | Description |
|
|
188
|
+
|--------|------|-------------|
|
|
189
|
+
| GET | /api/{resource}/ | List all items |
|
|
190
|
+
| GET | /api/{resource}/:id | Get single item |
|
|
191
|
+
| POST | /api/{resource}/ | Create item |
|
|
192
|
+
| PUT | /api/{resource}/:id | Update item |
|
|
193
|
+
| DELETE | /api/{resource}/:id | Delete item |
|
|
194
|
+
|
|
195
|
+
### Query Parameters for List
|
|
196
|
+
- `page` - Page number (default: 1)
|
|
197
|
+
- `limit` - Items per page (default: 10)
|
|
198
|
+
- `search` - Search term
|
|
199
|
+
- `sort_by` - Sort field
|
|
200
|
+
- `order` - Sort order (asc/desc)
|
|
201
|
+
|
|
202
|
+
## Configuration
|
|
203
|
+
|
|
204
|
+
### config.yaml
|
|
205
|
+
```yaml
|
|
206
|
+
server:
|
|
207
|
+
port: 8080
|
|
208
|
+
mode: debug # debug, release, test
|
|
209
|
+
|
|
210
|
+
database:
|
|
211
|
+
type: mysql # mysql, postgres, sqlite
|
|
212
|
+
host: localhost
|
|
213
|
+
port: 3306
|
|
214
|
+
user: root
|
|
215
|
+
password: ""
|
|
216
|
+
dbname: {project_name}
|
|
217
|
+
|
|
218
|
+
redis:
|
|
219
|
+
address: localhost:6379
|
|
220
|
+
password: ""
|
|
221
|
+
db: 0
|
|
222
|
+
|
|
223
|
+
jwt:
|
|
224
|
+
secret: your-secret-key
|
|
225
|
+
expiry: 24h
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Environment Variables
|
|
229
|
+
- `CONFIG_PATH` - Path to config file (default: ./config.yaml)
|
|
230
|
+
- `GIN_MODE` - Gin mode (debug/release)
|
|
231
|
+
|
|
232
|
+
## Testing
|
|
233
|
+
|
|
234
|
+
```go
|
|
235
|
+
// Use testify for assertions
|
|
236
|
+
func TestController_List(t *testing.T) {
|
|
237
|
+
router := setupTestRouter()
|
|
238
|
+
w := httptest.NewRecorder()
|
|
239
|
+
req, _ := http.NewRequest("GET", "/api/items/", nil)
|
|
240
|
+
router.ServeHTTP(w, req)
|
|
241
|
+
|
|
242
|
+
assert.Equal(t, 200, w.Code)
|
|
243
|
+
}
|
|
244
|
+
```
|