feat: add parapharmacy API with MongoDB and N8N setup

- Create parapharmacy-api microservice with Express + Swagger + MongoDB
- Add product model with full-text search and deduplication
- Add CRUD endpoints and bulk upsert for scrapers
- Update docker-compose.yml with MongoDB, N8N, and parapharmacy-api services
- Add proxy endpoints in FarmaFinder backend for parapharmacy
- Update frontend services to use parapharmacy API
- Add Swagger documentation at /api/docs
This commit is contained in:
Antoni Nuñez Romeu
2026-07-16 12:26:54 +02:00
parent 837f2cd4f3
commit b34657c78f
15 changed files with 1129 additions and 27 deletions
+130
View File
@@ -0,0 +1,130 @@
import express from 'express';
import cors from 'cors';
import mongoose from 'mongoose';
import morgan from 'morgan';
import rateLimit from 'express-rate-limit';
import swaggerJsdoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
import { config } from './config.js';
import productsRouter from './routes/products.js';
const app = express();
// Swagger configuration
const swaggerOptions = {
definition: {
openapi: '3.0.0',
info: {
title: 'FarmaFinder Parapharmacy API',
version: '1.0.0',
description: 'API for parapharmacy products from multiple Spanish pharmacies',
},
servers: [
{
url: `http://localhost:${config.port}`,
description: 'Development server',
},
],
components: {
schemas: {
Product: {
type: 'object',
properties: {
_id: { type: 'string' },
name: { type: 'string' },
brand: { type: 'string' },
category: { type: 'string' },
price: { type: 'number' },
source: { type: 'string' },
},
},
},
},
},
apis: ['./src/routes/*.js'],
};
const swaggerSpec = swaggerJsdoc(swaggerOptions);
// Middleware
app.use(cors(config.cors));
app.use(express.json({ limit: '10mb' }));
app.use(morgan('combined'));
// Rate limiting
const limiter = rateLimit({
windowMs: config.rateLimit.windowMs,
max: config.rateLimit.max,
standardHeaders: true,
legacyHeaders: false,
});
app.use(limiter);
// Swagger UI
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
explorer: true,
customCss: '.swagger-ui .topbar { display: none }',
customSiteTitle: 'FarmaFinder Parapharmacy API',
}));
// Routes
app.use('/api/products', productsRouter);
// Health check
app.get('/api/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
mongodb: mongoose.connection.readyState === 1 ? 'connected' : 'disconnected',
});
});
// Sources info
app.get('/api/sources', (req, res) => {
res.json(config.sources);
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error('Unhandled error:', err);
res.status(500).json({ error: 'Internal server error' });
});
// 404 handler
app.use((req, res) => {
res.status(404).json({ error: 'Not found' });
});
// Connect to MongoDB and start server
async function start() {
try {
console.log(`[Parapharmacy API] Connecting to MongoDB: ${config.mongodb.uri}`);
await mongoose.connect(config.mongodb.uri);
console.log('[Parapharmacy API] Connected to MongoDB');
app.listen(config.port, () => {
console.log(`[Parapharmacy API] Server running on port ${config.port}`);
console.log(`[Parapharmacy API] Swagger docs: http://localhost:${config.port}/api/docs`);
});
} catch (error) {
console.error('[Parapharmacy API] Failed to start:', error);
process.exit(1);
}
}
// Handle graceful shutdown
process.on('SIGTERM', async () => {
console.log('[Parapharmacy API] SIGTERM received, shutting down...');
await mongoose.disconnect();
process.exit(0);
});
process.on('SIGINT', async () => {
console.log('[Parapharmacy API] SIGINT received, shutting down...');
await mongoose.disconnect();
process.exit(0);
});
start();
export default app;