Files
cleanplate_api/bin/server.dart
Oracle Public Cloud User 6bd1ab7e9f 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.
2026-03-04 14:52:13 +00:00

148 lines
5.5 KiB
Dart

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);
}