API REST construida con FastAPI y Firestore (Firebase), diseñada para ser modular, escalable y mantenible.
Pensada para ser consumida por una app móvil Flutter.
- FastAPI
- Firebase Admin SDK (Firestore)
- Pydantic v2 (validaciones)
- Uvicorn
app/
api/
dependencies/ # Dependencias compartidas (auth, etc.)
v1/
endpoints/ # Controladores HTTP
router.py
core/ # Config, seguridad JWT, excepciones, Firebase client
repositories/ # Acceso a datos
schemas/ # Request/Response models + validaciones
services/ # Reglas de negocio
main.py
- Python 3.11+
- Proyecto Firebase ya configurado
- Archivo de credenciales de servicio para desarrollo local
- Crear entorno virtual e instalar dependencias:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt- Crear archivo
.env:
cp .env.example .env- Ajustar variables en
.env:
JWT_SECRET_KEYJWT_ALGORITHMACCESS_TOKEN_EXPIRE_MINUTESFIREBASE_PROJECT_IDFIREBASE_CREDENTIALS_JSON(recomendado para Render/produccion)FIREBASE_CREDENTIALS_PATHFIREBASE_DATABASE_URLFIREBASE_STORAGE_BUCKETAWS_REGIONS3_BUCKET_NAMEAWS_ACCESS_KEY_ID(opcional en AWS con IAM Role)AWS_SECRET_ACCESS_KEY(opcional en AWS con IAM Role)S3_PUBLIC_BASE_URL(opcional, por ejemplo CloudFront)S3_PRESIGNED_TTL_SECONDS(opcional, recomendado 900-3600)ALLOWED_ORIGINS
uvicorn app.main:app --reloadLa API ya incluye render.yaml para despliegue como Web Service.
Comando de arranque en Render:
uvicorn app.main:app --host 0.0.0.0 --port $PORTVariables recomendadas en Render:
ENVIRONMENT=productionAPP_DEBUG=falseJWT_SECRET_KEYFIREBASE_PROJECT_IDFIREBASE_CREDENTIALS_JSONFIREBASE_DATABASE_URLFIREBASE_STORAGE_BUCKETAWS_REGIONS3_BUCKET_NAMEAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYALLOWED_ORIGINS
Para Firebase en Render:
- usar
FIREBASE_CREDENTIALS_JSONcon el JSON completo de la service account en una variable secreta. - no depender de
firebase-service-account.jsonen produccion.
Documentación:
- Swagger:
http://localhost:8000/docs - ReDoc:
http://localhost:8000/redoc
GET /api/v1/healthGET /api/v1/health/firebasePOST /api/v1/auth/registerPOST /api/v1/auth/loginGET /api/v1/auth/meGET /api/v1/providers/me/homeGET /api/v1/providers/me/notificationsPATCH /api/v1/providers/me/notifications/read-allPATCH /api/v1/providers/me/notifications/{notification_id}/readDELETE /api/v1/providers/me/notificationsPOST /api/v1/providers/me/servicesPOST /api/v1/providers/me/services/draftsGET /api/v1/providers/me/servicesGET /api/v1/providers/me/services/{service_id}PATCH /api/v1/providers/me/services/{service_id}POST /api/v1/providers/me/services/{service_id}/imagesPATCH /api/v1/providers/me/services/{service_id}/images/mainPATCH /api/v1/providers/me/services/{service_id}/images/reorderDELETE /api/v1/providers/me/services/{service_id}/imagesDELETE /api/v1/providers/me/services/{service_id}POST /api/v1/providers/me/services/{service_id}/productsGET /api/v1/providers/me/services/{service_id}/productsGET /api/v1/providers/me/services/by-name/{service_name}/productsGET /api/v1/providers/me/services/{service_id}/products/{product_id}PATCH /api/v1/providers/me/services/{service_id}/products/{product_id}DELETE /api/v1/providers/me/services/{service_id}/products/{product_id}GET /api/v1/providers/me/products/reservationsDELETE /api/v1/providers/me/products/{product_id}POST /api/v1/providers/me/services/{service_id}/products/{product_id}/imagesPATCH /api/v1/providers/me/services/{service_id}/products/{product_id}/images/mainPATCH /api/v1/providers/me/services/{service_id}/products/{product_id}/images/reorderDELETE /api/v1/providers/me/services/{service_id}/products/{product_id}/imagesGET /api/v1/providers/me/products/{product_id}/availabilityPATCH /api/v1/providers/me/products/{product_id}/availability/{target_date}/blockPATCH /api/v1/providers/me/products/{product_id}/availability/{target_date}/unblockPOST /api/v1/providers/me/products/{product_id}/bookings/manualGET /api/v1/providers/me/bookingsGET /api/v1/providers/me/bookings/{booking_id}PATCH /api/v1/providers/me/bookings/{booking_id}PATCH /api/v1/providers/me/bookings/{booking_id}/statusGET /api/v1/providers/me/business-profilePUT /api/v1/providers/me/business-profilePOST /api/v1/providers/me/business-profile/logoPOST /api/v1/providers/me/business-profile/photosGET /api/v1/usersGET /api/v1/users/{user_id}PATCH /api/v1/users/{user_id}DELETE /api/v1/users/{user_id}
register: registra usuario confirst_name,last_name,email,password,confirm_passwordlogin: inicio de sesión conemailypassword- respuesta de auth incluye:
access_tokentoken_type(bearer)expires_inuser
- endpoints de
usersrequieren header:Authorization: Bearer <token>
- endpoints de
providersrequieren:Authorization: Bearer <token>- usuario con
role = provider
GET /api/v1/providers/me/business-profile: devuelve el perfil del negocio del proveedor autenticado. Si aún no existe, responde un perfil vacío. Incluyeis_onboarding_completedpara que el cliente sepa si debe volver a mostrar el onboarding.PUT /api/v1/providers/me/business-profile: crea o actualiza el perfil del negocio del proveedor autenticado.POST /api/v1/providers/me/business-profile/logo: sube el logo a S3. Recibemultipart/form-datacon campofileen formatoimage/jpeg,image/pngoimage/webpy con tamaño máximo de10 MB. El backend convierte la imagen a.webp.POST /api/v1/providers/me/business-profile/photos: sube una foto del negocio a S3. Recibemultipart/form-datacon campofileen formatoimage/jpeg,image/pngoimage/webpy con tamaño máximo de10 MB. El backend convierte la imagen a.webp.
GET /api/v1/providers/me/home: devuelve la información principal para la pantalla home del proveedor.GET /api/v1/providers/me/notifications: devuelve las notificaciones del proveedor autenticado y el contador de no leídas.PATCH /api/v1/providers/me/notifications/read-all: marca todas las notificaciones como leídas.PATCH /api/v1/providers/me/notifications/{notification_id}/read: marca una notificación como leída.DELETE /api/v1/providers/me/notifications: elimina todas las notificaciones del proveedor autenticado.
Respuesta ejemplo de GET /api/v1/providers/me/home:
{
"provider_id": "provider_123",
"display_name": "Jair",
"business_name": "Sonido Fiesta",
"avatar_url": "https://storage.example.com/logo.webp",
"quick_stats": {
"reservations_this_month": 0,
"active_services": 0
},
"featured_services": []
}Respuesta ejemplo de GET /api/v1/providers/me/notifications:
{
"items": [],
"unread_count": 0
}POST /api/v1/providers/me/services: crea un servicio padre.POST /api/v1/providers/me/services/drafts: crea un borrador mínimo de servicio para flujos comoCreateServiceView.GET /api/v1/providers/me/services: lista los servicios padre del proveedor autenticado.GET /api/v1/providers/me/services/{service_id}: obtiene el detalle de un servicio padre.PATCH /api/v1/providers/me/services/{service_id}: actualiza parcialmente un servicio padre.POST /api/v1/providers/me/services/{service_id}/images: sube una imagen del servicio padre a S3. Recibemultipart/form-dataconfileyis_main. Aceptajpg/png/webp, límite máximo10 MB, y la convierte a.webp.PATCH /api/v1/providers/me/services/{service_id}/images/main: cambia la foto principal del servicio padre usandoimage_url.PATCH /api/v1/providers/me/services/{service_id}/images/reorder: reordena las imágenes existentes del servicio padre usando la lista completa deimage_urls.DELETE /api/v1/providers/me/services/{service_id}/images: elimina una imagen específica del servicio padre usandoimage_url.DELETE /api/v1/providers/me/services/{service_id}: elimina el servicio padre, sus productos hijos y sus imágenes en S3.
Categorías soportadas:
djphotographyentertainmentbanquetfurnitureequipmentvenuedecoration
Payload ejemplo para POST /api/v1/providers/me/services:
{
"category": "photography",
"name": "Fotografía",
"description": "Servicio principal de fotografía para eventos.",
"status": "active"
}Payload ejemplo para POST /api/v1/providers/me/services/drafts:
{
"category": "dj",
"name": "DJ para eventos",
"description": "Servicio base para eventos sociales."
}POST /api/v1/providers/me/services/{service_id}/products: crea un producto hijo del servicio.GET /api/v1/providers/me/services/{service_id}/products: lista los productos hijos del servicio.GET /api/v1/providers/me/services/by-name/{service_name}/products: lista los productos hijos usando el nombre del servicio. Es útil para pantallas que todavía no navegan conservice_id. Si el proveedor tiene servicios duplicados con el mismo nombre, responde conflicto.GET /api/v1/providers/me/services/{service_id}/products/{product_id}: obtiene el detalle de un producto.PATCH /api/v1/providers/me/services/{service_id}/products/{product_id}: actualiza parcialmente un producto.DELETE /api/v1/providers/me/services/{service_id}/products/{product_id}: elimina un producto y sus imágenes.GET /api/v1/providers/me/products/reservations: devuelve el resumen global de productos para la pantalla de reservas, incluyendo la próxima reserva por producto.DELETE /api/v1/providers/me/products/{product_id}: elimina un producto usando soloproduct_id, útil para flujos de resumen global.POST /api/v1/providers/me/services/{service_id}/products/{product_id}/images: sube una imagen del producto a S3. Recibemultipart/form-dataconfileyis_main. Aceptajpg/png/webp, límite máximo10 MB, y la convierte a.webp.PATCH /api/v1/providers/me/services/{service_id}/products/{product_id}/images/main: cambia la foto principal del producto usandoimage_url.PATCH /api/v1/providers/me/services/{service_id}/products/{product_id}/images/reorder: reordena las imágenes del producto usando la lista completa deimage_urls.DELETE /api/v1/providers/me/services/{service_id}/products/{product_id}/images: elimina una imagen específica del producto usandoimage_url.
Payload ejemplo para POST /api/v1/providers/me/services/{service_id}/products:
{
"name": "Cobertura premium",
"description": "Cobertura completa para boda con entrega digital.",
"price": 4500,
"pricing_unit": "Por evento",
"approx_photos": 300,
"delivery_time": "15 días",
"min_duration": "4 horas",
"extra_hour_allowed": true,
"extra_hour_price": 500,
"inclusions": [
"Edición básica",
"Galería digital"
],
"policies": [
"50% de anticipo",
"No reembolsable en cancelación"
]
}Respuesta ejemplo de GET /api/v1/providers/me/products/reservations:
{
"items": [
{
"id": "prod_123",
"service_id": "service_123",
"product_name": "Cobertura premium",
"category": "photography",
"image_url": "https://storage.example.com/product.webp",
"next_booking": {
"booking_id": "booking_123",
"customer_name": "Juan Perez",
"customer_image_url": "",
"date": "2026-03-28",
"status": "Confirmada"
}
}
],
"total": 1
}GET /api/v1/providers/me/products/{product_id}/availability: devuelve el calendario mensual de disponibilidad del producto. Requiereyearymonth.PATCH /api/v1/providers/me/products/{product_id}/availability/{target_date}/block: bloquea manualmente una fecha del producto.PATCH /api/v1/providers/me/products/{product_id}/availability/{target_date}/unblock: desbloquea una fecha bloqueada manualmente.- El estado
reservedse calcula a partir de reservas confirmadas del producto, incluidas las reservas manuales del proveedor.
Ejemplo:
GET /api/v1/providers/me/products/prod_123/availability?year=2026&month=3
Respuesta ejemplo:
{
"product_id": "prod_123",
"product_name": "Cobertura premium",
"year": 2026,
"month": 3,
"days": [
{
"date": "2026-03-01",
"status": "available",
"booking": null
},
{
"date": "2026-03-02",
"status": "reserved",
"booking": {
"booking_id": "booking_123",
"customer_name": "Juan Perez",
"customer_image_url": "",
"event_type": "Boda",
"guests": 150
}
}
]
}POST /api/v1/providers/me/products/{product_id}/bookings/manual: crea una reserva manual confirmada y marca la fecha comoreserveden disponibilidad.GET /api/v1/providers/me/bookings: lista reservas del proveedor autenticado. Acepta filtros opcionalesstatus,year,monthyproduct_id.GET /api/v1/providers/me/bookings/{booking_id}: obtiene el detalle de una reserva.PATCH /api/v1/providers/me/bookings/{booking_id}: modifica los datos editables de una reserva, incluidos montos y fecha. Si la reserva está confirmada y cambia de fecha, también se actualiza el calendario.PATCH /api/v1/providers/me/bookings/{booking_id}/status: actualiza el estado de una reserva. Cuando una reserva pasa aconfirmedreserva la fecha del calendario; si sale deconfirmed, libera esa fecha.
Payload ejemplo para POST /api/v1/providers/me/products/{product_id}/bookings/manual:
{
"customer_name": "Juan Perez",
"event_date": "2026-03-28",
"has_specific_schedule": true,
"start_time": "18:30",
"end_time": "23:00",
"event_type": "Boda",
"guests": 150,
"contact_phone": "55 1234 5678",
"contact_email": "cliente@correo.com",
"event_location": "Jardin Las Palmas, Monterrey",
"payment_details": "Anticipo de $2,000 recibido, resto pendiente",
"total_amount": 8000,
"paid_amount": 2000,
"notes": "Anticipo liquidado. Montaje a las 16:00."
}Respuesta ejemplo:
{
"id": "booking_123",
"provider_id": "provider_123",
"service_id": "service_123",
"service_name": "Fotografia",
"product_id": "prod_123",
"product_name": "Cobertura premium",
"customer_name": "Juan Perez",
"customer_image_url": "",
"event_date": "2026-03-28",
"has_specific_schedule": true,
"start_time": "18:30:00",
"end_time": "23:00:00",
"event_type": "Boda",
"guests": 150,
"contact_phone": "55 1234 5678",
"contact_email": "cliente@correo.com",
"event_location": "Jardin Las Palmas, Monterrey",
"payment_details": "Anticipo de $2,000 recibido, resto pendiente",
"total_amount": 8000,
"paid_amount": 2000,
"pending_amount": 6000,
"time_label": "18:30 - 23:00",
"status_label": "Confirmada",
"notes": "Anticipo liquidado. Montaje a las 16:00.",
"source": "manual",
"status": "confirmed",
"created_at": "2026-03-23T12:00:00Z",
"updated_at": "2026-03-23T12:00:00Z"
}Payload ejemplo para PATCH /api/v1/providers/me/bookings/{booking_id}:
{
"event_date": "2026-03-29",
"start_time": "19:00",
"end_time": "23:30",
"total_amount": 9000,
"paid_amount": 3000,
"notes": "Cliente solicita una hora extra."
}Payload ejemplo para PATCH /api/v1/providers/me/bookings/{booking_id}/status:
{
"status": "cancelled"
}Payload ejemplo para PATCH /api/v1/providers/me/services/{service_id}/images/main:
{
"image_url": "https://firebasestorage.googleapis.com/..."
}Payload ejemplo para PATCH /api/v1/providers/me/services/{service_id}/images/reorder:
{
"image_urls": [
"https://firebasestorage.googleapis.com/...img2.webp",
"https://firebasestorage.googleapis.com/...img1.webp"
]
}Payload ejemplo:
{
"business_name": "Mi Negocio",
"location": "Teziutlán, Puebla",
"coverage_area": "Teziutlán y municipios cercanos",
"contact_number": "+522221112233",
"whatsapp": "+522221112233",
"instagram": "@mi_negocio",
"facebook": "Mi Negocio",
"website": "https://minegocio.com",
"logo_url": "https://storage.example.com/logo.jpg",
"photo_urls": [
"https://storage.example.com/photo-1.jpg",
"https://storage.example.com/photo-2.jpg"
]
}Archivos subidos:
- se guardan en S3 usando la ruta lógica
providers/... - se devuelve URL pública con este orden:
- si defines
S3_PUBLIC_BASE_URL, se usa esa base - si no, se usa
https://<bucket>.s3.<region>.amazonaws.com/<key>
Dependencias nuevas:
python-multipartPillowboto3
first_nameylast_name: solo letras, normalización de espacios y capitalizaciónemail: formato válido y normalización a minúsculaspassword: mínimo 8 caracteresconfirm_password: debe coincidir conpasswordphone: formato internacional E.164 (opcional)birth_date: fecha válida (opcional)- actualización parcial: al menos un campo requerido
- unicidad de email en registro
Registro:
{
"first_name": "Alan",
"last_name": "Hernandez",
"email": "alan@email.com",
"password": "MyStrongPass123",
"confirm_password": "MyStrongPass123"
}Login:
{
"email": "alan@email.com",
"password": "MyStrongPass123"
}- Separación por capas (
endpoint -> service -> repository) - Configuración por entorno con
.env - Manejo centralizado de errores de dominio
- Respuestas tipadas con Pydantic
- CORS configurable para Flutter y otros clientes
- Mantener
.envfuera del repositorio - En nube, ajustar valores de entorno sin modificar código
- Cambiar
FIREBASE_DATABASE_URL,AWS_REGION,S3_BUCKET_NAMEy credenciales por las del entorno destino - Para producción se recomienda credenciales gestionadas por proveedor cloud (IAM/Secrets)
- Incorporar refresh tokens y revocación de sesiones.
- Agregar tests automatizados (unit + integración).
- Agregar módulos nuevos por dominio (
events,bookings,payments) siguiendo el mismo patrón por capas. - Añadir observabilidad (logging estructurado y métricas).