TechLead
Lección 7 de 8
8 min de lectura
Firebase

Cloud Functions

Ejecuta código backend serverless en respuesta a eventos de Firebase y solicitudes HTTP

¿Qué son Cloud Functions?

Cloud Functions for Firebase te permiten ejecutar código backend automáticamente en respuesta a eventos disparados por Firebase y solicitudes HTTPS. Tu código se almacena en la nube de Google y se ejecuta en un entorno gestionado, por lo que no necesitas administrar ni escalar servidores.

Las funciones pueden activarse por eventos de servicios de Firebase (Auth, Firestore, Storage, etc.), solicitudes HTTP, horarios programados (cron) o servicios de Google Cloud.

⚡ Casos de uso comunes

  • 🔔 Notificaciones: Enviar notificaciones push cuando cambian datos.
  • 📧 Emails: Enviar emails de bienvenida al registrarse.
  • 🖼️ Procesamiento de imágenes: Generar miniaturas al subir imágenes.
  • 🔒 Validación de datos: Validar y sanitizar datos antes de guardar.
  • 🌐 APIs: Construir APIs REST y webhooks.
  • ⏰ Tareas programadas: Ejecutar trabajos de limpieza en horarios definidos.

Configurar Cloud Functions

Inicializa Cloud Functions en tu proyecto:

# Initialize functions in your project
firebase init functions

# Choose options:
# - JavaScript or TypeScript
# - ESLint (recommended)
# - Install dependencies

Esto crea la estructura del directorio de funciones:

my-project/
├── functions/
│   ├── src/
│   │   └── index.ts        # Function entry point
│   ├── package.json
│   └── tsconfig.json
├── firebase.json
└── ...

Funciones HTTP

Crea funciones que respondan a solicitudes HTTP:

import { onRequest } from 'firebase-functions/v2/https';

// Simple HTTP function
export const helloWorld = onRequest((request, response) => {
  response.send('Hello from Firebase!');
});

// With CORS enabled
export const api = onRequest({ cors: true }, (request, response) => {
  const { name } = request.query;
  response.json({ message: `Hello, ${name || 'World'}!` });
});

// Handle different HTTP methods
export const users = onRequest({ cors: true }, async (request, response) => {
  switch (request.method) {
    case 'GET':
      // Handle GET request
      response.json({ users: [] });
      break;
    case 'POST':
      // Handle POST request
      const { email, name } = request.body;
      response.json({ id: 'new-user-id', email, name });
      break;
    default:
      response.status(405).send('Method Not Allowed');
  }
});

Triggers de Firestore

Ejecuta funciones cuando cambian documentos en Firestore:

import { onDocumentCreated, onDocumentUpdated, onDocumentDeleted } from 'firebase-functions/v2/firestore';
import { getFirestore } from 'firebase-admin/firestore';

// Initialize admin SDK
import { initializeApp } from 'firebase-admin/app';
initializeApp();

const db = getFirestore();

// Trigger when a document is created
export const onUserCreated = onDocumentCreated('users/{userId}', async (event) => {
  const snapshot = event.data;
  if (!snapshot) return;

  const userData = snapshot.data();
  const userId = event.params.userId;

  // Create a profile document
  await db.collection('profiles').doc(userId).set({
    displayName: userData.name,
    createdAt: new Date(),
    postsCount: 0
  });

  console.log(`Profile created for user: ${userId}`);
});

// Trigger when a document is updated
export const onUserUpdated = onDocumentUpdated('users/{userId}', async (event) => {
  const before = event.data?.before.data();
  const after = event.data?.after.data();

  // Check what changed
  if (before?.email !== after?.email) {
    console.log(`Email changed from ${before?.email} to ${after?.email}`);
    // Send email verification
  }
});

// Trigger when a document is deleted
export const onUserDeleted = onDocumentDeleted('users/{userId}', async (event) => {
  const userId = event.params.userId;

  // Clean up related data
  await db.collection('profiles').doc(userId).delete();
  await db.collection('posts').where('authorId', '==', userId).get()
    .then(snapshot => {
      const batch = db.batch();
      snapshot.docs.forEach(doc => batch.delete(doc.ref));
      return batch.commit();
    });
});

Triggers de autenticación

Ejecuta funciones cuando usuarios se registran o eliminan su cuenta:

import { beforeUserCreated, beforeUserSignedIn } from 'firebase-functions/v2/identity';
import { onCall } from 'firebase-functions/v2/https';
import { getAuth } from 'firebase-admin/auth';

// Before user is created (can modify or block)
export const validateNewUser = beforeUserCreated((event) => {
  const user = event.data;

  // Block sign-ups from certain domains
  if (user.email?.endsWith('@blocked.com')) {
    throw new Error('This email domain is not allowed');
  }

  // Add custom claims
  return {
    customClaims: {
      role: 'user',
      createdVia: 'signup'
    }
  };
});

// Callable function to set admin role
export const setAdminRole = onCall(async (request) => {
  // Check if caller is admin
  if (request.auth?.token.role !== 'admin') {
    throw new Error('Only admins can set admin roles');
  }

  const { userId } = request.data;
  await getAuth().setCustomUserClaims(userId, { role: 'admin' });

  return { success: true };
});

Triggers de Storage

Procesa archivos cuando se suben a Storage:

import { onObjectFinalized, onObjectDeleted } from 'firebase-functions/v2/storage';
import { getStorage } from 'firebase-admin/storage';
import * as path from 'path';

// Trigger when a file is uploaded
export const processImage = onObjectFinalized(async (event) => {
  const filePath = event.data.name;
  const contentType = event.data.contentType;

  // Only process images
  if (!contentType?.startsWith('image/')) {
    console.log('Not an image, skipping');
    return;
  }

  // Skip if already a thumbnail
  if (filePath?.includes('thumb_')) {
    return;
  }

  const bucket = getStorage().bucket(event.data.bucket);
  const fileName = path.basename(filePath || '');
  const fileDir = path.dirname(filePath || '');

  // Download, resize, and upload thumbnail
  const tempFilePath = `/tmp/${fileName}`;
  await bucket.file(filePath!).download({ destination: tempFilePath });

  // Use sharp or another library for image processing
  // const sharp = require('sharp');
  // await sharp(tempFilePath).resize(200, 200).toFile(thumbPath);

  const thumbFileName = `thumb_${fileName}`;
  const thumbFilePath = path.join(fileDir, thumbFileName);

  await bucket.upload(tempFilePath, {
    destination: thumbFilePath,
    metadata: { contentType }
  });

  console.log(`Thumbnail created: ${thumbFilePath}`);
});

// Trigger when a file is deleted
export const onFileDeleted = onObjectDeleted(async (event) => {
  const filePath = event.data.name;
  console.log(`File deleted: ${filePath}`);

  // Delete associated thumbnail if exists
  if (!filePath?.includes('thumb_')) {
    const bucket = getStorage().bucket(event.data.bucket);
    const fileName = path.basename(filePath || '');
    const fileDir = path.dirname(filePath || '');
    const thumbPath = path.join(fileDir, `thumb_${fileName}`);

    try {
      await bucket.file(thumbPath).delete();
    } catch (error) {
      // Thumbnail might not exist
    }
  }
});

Funciones programadas

Ejecuta funciones en un horario:

import { onSchedule } from 'firebase-functions/v2/scheduler';
import { getFirestore } from 'firebase-admin/firestore';

// Run every day at midnight
export const dailyCleanup = onSchedule('0 0 * * *', async (event) => {
  const db = getFirestore();

  // Delete documents older than 30 days
  const thirtyDaysAgo = new Date();
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

  const oldDocs = await db.collection('logs')
    .where('createdAt', '<', thirtyDaysAgo)
    .get();

  const batch = db.batch();
  oldDocs.docs.forEach(doc => batch.delete(doc.ref));
  await batch.commit();

  console.log(`Deleted ${oldDocs.size} old documents`);
});

// Run every hour
export const hourlySync = onSchedule('0 * * * *', async (event) => {
  console.log('Running hourly sync...');
  // Sync data with external service
});

// Run every Monday at 9 AM
export const weeklyReport = onSchedule('0 9 * * 1', async (event) => {
  console.log('Generating weekly report...');
  // Generate and send report
});

Funciones callable

Funciones que pueden llamarse directamente desde el cliente:

// functions/src/index.ts
import { onCall, HttpsError } from 'firebase-functions/v2/https';

export const addNumbers = onCall((request) => {
  const { a, b } = request.data;

  // Validate input
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new HttpsError('invalid-argument', 'Both arguments must be numbers');
  }

  return { result: a + b };
});

// Protected callable function
export const getUserData = onCall(async (request) => {
  // Check if user is authenticated
  if (!request.auth) {
    throw new HttpsError('unauthenticated', 'User must be logged in');
  }

  const userId = request.auth.uid;
  const db = getFirestore();

  const userDoc = await db.collection('users').doc(userId).get();

  if (!userDoc.exists) {
    throw new HttpsError('not-found', 'User not found');
  }

  return userDoc.data();
});
// Client-side usage
import { getFunctions, httpsCallable } from 'firebase/functions';

const functions = getFunctions();

// Call the function
const addNumbers = httpsCallable(functions, 'addNumbers');
const result = await addNumbers({ a: 5, b: 3 });
console.log(result.data.result); // 8

// Call protected function
const getUserData = httpsCallable(functions, 'getUserData');
const userData = await getUserData();
console.log(userData.data);

Configuración de entorno

Administra secretos y configuración:

import { defineSecret, defineString } from 'firebase-functions/params';

// Define secrets (stored securely)
const apiKey = defineSecret('API_KEY');
const dbPassword = defineSecret('DB_PASSWORD');

// Define config values
const welcomeMessage = defineString('WELCOME_MESSAGE', {
  default: 'Welcome to our app!'
});

// Use in function
export const secureFunction = onRequest(
  { secrets: [apiKey, dbPassword] },
  async (request, response) => {
    // Access secret values
    const key = apiKey.value();
    const password = dbPassword.value();

    // Use the secrets...
    response.json({ message: welcomeMessage.value() });
  }
);

// Set secrets via CLI:
// firebase functions:secrets:set API_KEY
// firebase functions:secrets:set DB_PASSWORD

Desplegar funciones

Despliega tus funciones en Firebase:

# Deploy all functions
firebase deploy --only functions

# Deploy specific function
firebase deploy --only functions:helloWorld

# Deploy multiple specific functions
firebase deploy --only functions:helloWorld,functions:api

# View function logs
firebase functions:log

# View logs for specific function
firebase functions:log --only helloWorld

Pruebas locales con el emulador

Prueba funciones localmente antes de desplegar:

# Start the emulator
firebase emulators:start --only functions

# Or start all emulators
firebase emulators:start

# Functions will be available at:
# http://localhost:5001/YOUR_PROJECT/us-central1/functionName

# Call HTTP functions
curl http://localhost:5001/my-project/us-central1/helloWorld

# View emulator UI
# http://localhost:4000

💡 Puntos clave

  • • Cloud Functions ejecuta código backend serverless en respuesta a eventos
  • • Las funciones HTTP crean APIs REST y webhooks
  • • Triggers de Firestore/Auth/Storage responden a cambios de datos
  • • Funciones programadas se ejecutan con cron
  • • Las funciones callable se invocan directamente desde el cliente
  • • Usa el emulador para probar localmente antes de desplegar

📚 Más recursos

Continuar Aprendiendo