Initial commit: CleanPlate backend API
Dart Shelf REST API with auth, recipes, AI (Claude), search, and community modules. PostgreSQL, Redis, Meilisearch. Docker Compose for local dev.
This commit is contained in:
147
bin/server.dart
Normal file
147
bin/server.dart
Normal file
@@ -0,0 +1,147 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
import 'package:cleanplate_api/src/config/env.dart';
|
||||
import 'package:cleanplate_api/src/config/database.dart';
|
||||
import 'package:cleanplate_api/src/config/redis_config.dart';
|
||||
|
||||
import 'package:cleanplate_api/src/middleware/auth_middleware.dart';
|
||||
import 'package:cleanplate_api/src/middleware/cors_middleware.dart';
|
||||
import 'package:cleanplate_api/src/middleware/logging_middleware.dart';
|
||||
import 'package:cleanplate_api/src/middleware/error_handler.dart';
|
||||
import 'package:cleanplate_api/src/middleware/rate_limit_middleware.dart';
|
||||
|
||||
import 'package:cleanplate_api/src/modules/auth/auth_routes.dart';
|
||||
import 'package:cleanplate_api/src/modules/recipes/recipe_routes.dart';
|
||||
import 'package:cleanplate_api/src/modules/ai/ai_routes.dart';
|
||||
import 'package:cleanplate_api/src/modules/search/search_routes.dart';
|
||||
import 'package:cleanplate_api/src/modules/media/media_routes.dart';
|
||||
import 'package:cleanplate_api/src/modules/users/user_routes.dart';
|
||||
import 'package:cleanplate_api/src/modules/community/community_routes.dart';
|
||||
|
||||
final _log = Logger('Server');
|
||||
|
||||
void main(List<String> args) async {
|
||||
// ---------------------------------------------------------------
|
||||
// Logging
|
||||
// ---------------------------------------------------------------
|
||||
Logger.root.level = Env.isProduction ? Level.INFO : Level.ALL;
|
||||
Logger.root.onRecord.listen((record) {
|
||||
stderr.writeln(
|
||||
'${record.time} [${record.level.name}] ${record.loggerName}: ${record.message}');
|
||||
if (record.error != null) stderr.writeln(' Error: ${record.error}');
|
||||
if (record.stackTrace != null) {
|
||||
stderr.writeln(' Stack: ${record.stackTrace}');
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Infrastructure connections
|
||||
// ---------------------------------------------------------------
|
||||
try {
|
||||
await Database.initialize();
|
||||
_log.info('Database connected');
|
||||
} catch (e) {
|
||||
_log.warning('Could not connect to database: $e');
|
||||
_log.warning('Server will start but database-dependent routes will fail');
|
||||
}
|
||||
|
||||
try {
|
||||
await RedisConfig.initialize();
|
||||
_log.info('Redis connected');
|
||||
} catch (e) {
|
||||
_log.warning('Could not connect to Redis: $e');
|
||||
_log.warning('Server will start but rate-limiting may use in-memory fallback');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Routers
|
||||
// ---------------------------------------------------------------
|
||||
final authRoutes = AuthRoutes();
|
||||
final recipeRoutes = RecipeRoutes();
|
||||
final aiRoutes = AiRoutes();
|
||||
final searchRoutes = SearchRoutes();
|
||||
final mediaRoutes = MediaRoutes();
|
||||
final userRoutes = UserRoutes();
|
||||
final communityRoutes = CommunityRoutes();
|
||||
|
||||
// Top-level router.
|
||||
final app = Router();
|
||||
|
||||
// Health check.
|
||||
app.get('/health', (Request request) {
|
||||
return Response.ok(
|
||||
jsonEncode({'status': 'ok'}),
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
});
|
||||
|
||||
// Mount module routers under /api/v1.
|
||||
app.mount('/api/v1/auth/', authRoutes.router.call);
|
||||
app.mount('/api/v1/recipes/', recipeRoutes.router.call);
|
||||
app.mount('/api/v1/ai/', aiRoutes.router.call);
|
||||
app.mount('/api/v1/search/', searchRoutes.router.call);
|
||||
app.mount('/api/v1/media/', mediaRoutes.router.call);
|
||||
app.mount('/api/v1/users/', userRoutes.router.call);
|
||||
|
||||
// Community reviews are nested under a recipe.
|
||||
// We use a small adapter to inject the recipeId into the request context.
|
||||
app.mount('/api/v1/recipes/<recipeId>/reviews/', (Request request) {
|
||||
final recipeId = request.params['recipeId'];
|
||||
final updated = request.change(context: {'recipeId': recipeId});
|
||||
return communityRoutes.router.call(updated);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Middleware pipeline
|
||||
// ---------------------------------------------------------------
|
||||
final handler = Pipeline()
|
||||
.addMiddleware(corsMiddleware())
|
||||
.addMiddleware(loggingMiddleware())
|
||||
.addMiddleware(errorHandler())
|
||||
.addMiddleware(rateLimitMiddleware())
|
||||
.addMiddleware(authMiddleware())
|
||||
.addHandler(app.call);
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Start server
|
||||
// ---------------------------------------------------------------
|
||||
final ip = InternetAddress.anyIPv4;
|
||||
final port = int.parse(Env.port);
|
||||
final server = await shelf_io.serve(handler, ip, port);
|
||||
|
||||
_log.info('CleanPlate API listening on http://${server.address.host}:${server.port}');
|
||||
_log.info('Environment: ${Env.environment}');
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Graceful shutdown
|
||||
// ---------------------------------------------------------------
|
||||
ProcessSignal.sigint.watch().listen((_) async {
|
||||
_log.info('SIGINT received — shutting down...');
|
||||
await _shutdown(server);
|
||||
});
|
||||
|
||||
// SIGTERM is not available on Windows but fine on Linux/macOS.
|
||||
if (!Platform.isWindows) {
|
||||
ProcessSignal.sigterm.watch().listen((_) async {
|
||||
_log.info('SIGTERM received — shutting down...');
|
||||
await _shutdown(server);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _shutdown(HttpServer server) async {
|
||||
_log.info('Closing HTTP server...');
|
||||
await server.close(force: false);
|
||||
_log.info('Closing database pool...');
|
||||
await Database.close();
|
||||
_log.info('Closing Redis connection...');
|
||||
await RedisConfig.close();
|
||||
_log.info('Shutdown complete');
|
||||
exit(0);
|
||||
}
|
||||
Reference in New Issue
Block a user