Inicio / Artículos / Firebase Auth y Firestore en Next.js sin colecciones planas

Firebase Auth y Firestore en Next.js sin colecciones planas

Guarde una única instancia de la aplicación Firebase, permita que los ayudantes de autenticación lancen errores tipados, anide los entrenamientos bajo cada usuario y pruebe las operaciones de escritura con los emuladores.

1049 palabras

Los proyectos iniciales heredados de Firebase + Next.js suelen presentar un par de problemas: una colección plana workouts que alberga los documentos de cada cuenta, y bloques de captura que registran el error y devuelven undefined, por lo que quienes llaman a la función nunca se enteran de que la escritura falló. Las demostraciones con un único inicio de sesión ocultan ambos problemas. El tráfico en producción no lo hace.

El núcleo de autenticación y seguimiento de una aplicación de fitness fue reconstruido con el SDK de JavaScript de Firebase 12.17.1 y probado en los emuladores de Firebase para poder verificar las operaciones de escritura de extremo a extremo. Las notas siguientes se centran en corregir la estructura desde el principio.

Una instancia de Firebase, no cinco

Un error frecuente es llamar a initializeApp al principio de un módulo que son importados por varias rutas. El recargue en tiempo real de Next.js, sumado a la separación entre los paquetes del servidor y del cliente, puede cargar ese módulo dos veces; la segunda llamada genera un error indicando que ya existe una aplicación predeterminada. Hay que protegerla:

import { initializeApp, getApps, getApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET!,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_SENDER_ID!,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID!,
};const app = getApps().length ? getApp() : initializeApp(firebaseConfig);export const auth = getAuth(app);
export const db = getFirestore(app);

getApps().length ? getApp() : initializeApp(...) es toda la protección necesaria. Coloque la configuración en las variables de entorno NEXT_PUBLIC_ en lugar de codificarla directamente; no porque la configuración de Firebase para web sea secreta (está diseñada para ser enviada al navegador), sino porque los proyectos separados para desarrollo y producción no deberían requerir editar el código fuente para cambiarla.

Los tutoriales antiguos pegan cadenas como "your-api-key" en el archivo. Eso por sí mismo no constituye una brecha de seguridad; es un hábito que eventualmente lleva a que la configuración real de producción termine en un repositorio público.

Autenticación que devuelve algo utilizable por quien la llama

Utilice esta estructura. Observe qué evita hacer: capturar el error y devolver undefined.

import {
  createUserWithEmailAndPassword,
  type User,
} from "firebase/auth";
import { addDoc, collection, serverTimestamp } from "firebase/firestore";
import { auth, db } from "./firebase";
export async function registerUser(
  email: string,
  password: string,
): Promise<User> {
  const cred = await createUserWithEmailAndPassword(auth, email, password);
  return cred.user;
}

Los ayudantes antiguos envuelven la llamada en try/catch, imprimen un error de registro y devuelven undefined. El código de la interfaz gráfica luego espera a que se ejecute registerUser(...) y accede a user.uid, lo que puede resultar en un valor ausente; por eso, una contraseña incorrecta o un correo electrónico duplicado nunca se convierten en un error de autenticación claro, sino que aparecen más tarde al intentar leer una propiedad de undefined, lejos del verdadero problema.

Es mejor rechazar la solicitud. La llamada de autenticación de Firebase genera errores específicos (auth/email-already-in-use, auth/weak-password y similares). Asigne esos códigos a mensajes legibles en el formulario. Haga que el helper de datos sea honesto: funcione correctamente o lance un error.

Estructurar los entrenamientos por usuario desde el primer día

Este cambio es especialmente importante más allá de un prototipo. Los ejemplos antiguos crean una colección plana workouts con un campo userId:

// what everyone copies — one collection for the whole app
addDoc(collection(db, "workouts"), { userId, ...workout });

Al realizar una consulta de “mis entrenamientos” contra esa colección, se escanea un conjunto que crece junto con toda la base de usuarios, y las reglas de seguridad deben filtrar por userId en cada operación. Es preferible utilizar una subcolección para que los entrenamientos de cada usuario estén dentro de su propio documento:

export type WorkoutInput = {
  type: string;
  durationMinutes: number;
  caloriesBurned: number;
};
export async function addWorkoutSession(
  userId: string,
  workout: WorkoutInput,
): Promise<string> {
  const ref = await addDoc(collection(db, "users", userId, "workouts"), {
    ...workout,
    createdAt: serverTimestamp(),
  });
  return ref.id;
}

collection(db, “users”, userId, “workouts”) apunta a la subcolección privada de esa cuenta. Las reglas de seguridad pueden entonces comparar request.auth.uid con el segmento de ruta {uid} tanto en lecturas como en escrituras, de modo que cada consulta permanezca dentro de los documentos de un único usuario.

Dos detalles que merecen destacarse. Prefiera serverTimestamp() en lugar de new Date(): los relojes del cliente (o clientes malintencionados) generan marcas de tiempo incorrectas; serverTimestamp() es un valor de referencia que Firestore llena con la hora del servidor al realizar el registro y no puede ser falsificado. Al especificar el payload como WorkoutInput en lugar de any, se detectan errores tipográficos en los nombres de campos que, de lo contrario, solo se manifestarían cuando un gráfico no se renderiza silenciosamente.

Demuestre que realmente se escribe

No confíe en el código de Firebase que nunca se haya ejecutado en el emulador: el SDK aceptará llamadas que una regla de seguridad real rechazaría. Dirija el SDK hacia los emuladores locales y ejecute todo el proceso: registro, escritura y lectura posterior.

import { getAuth, connectAuthEmulator, createUserWithEmailAndPassword } from "firebase/auth";
import { getFirestore, connectFirestoreEmulator, addDoc, getDocs, collection, serverTimestamp } from "firebase/firestore";
connectAuthEmulator(auth, "http://127.0.0.1:9099", { disableWarnings: true });
connectFirestoreEmulator(db, "127.0.0.1", 8080);const cred = await createUserWithEmailAndPassword(auth, email, "s3cret-pass");
const ref = await addDoc(
  collection(db, "users", cred.user.uid, "workouts"),
  { type: "run", durationMinutes: 32, caloriesBurned: 410, createdAt: serverTimestamp() },
);
const snap = await getDocs(collection(db, "users", cred.user.uid, "workouts"));

Ejecutar el mismo camino a través del ejecutor del emulador generó un uid de autenticación concreto, un id de escritura y un documento de lectura cuyo createdAt era una marca de tiempo real de Firestore (segundos/nanosegundos) en lugar del valor de referencia, lo que demuestra que el servidor llenó ese campo. Los caminos o tipos de campos incorrectos fallan en la computadora portátil, no después del despliegue.

Advertencia sobre las herramientas: la CLI de Firebase ahora requiere Java 21 o una versión más reciente. Un JRE antiguo en una computadora portátil limpia detendrá al ejecutor del emulador con un error de versión antes de que se ejecute cualquier código de la aplicación. Actualice el JDK y vuelva a intentarlo.

Siguiente paso

La autenticación sumada a una operación de escritura con alcance correcto constituye la base. Un producto terminado suele conectar onAuthStateChanged para gestionar el estado de la interfaz cuando el usuario está conectado, mapear los códigos de error de autenticación en el formulario y mostrar el historial mediante getDocs, orderBy("createdAt", "desc") y un limit. Todas estas funcionalidades siguen dependiendo de dos decisiones tomadas inicialmente: inicializar Firebase una sola vez detrás de un mecanismo de protección y almacenar los documentos relacionados con los entrenamientos bajo el usuario propietario. Si se omite alguna de ellas, las funcionalidades posteriores se verán afectadas.

Lecturas relacionadas

  • Comprendiendo los ganchos personalizados de React: reutilización de lógica sin estado compartido — Aprenda qué son los ganchos personalizados de React, cómo extraen y comparten lógica con estado entre componentes, y qué errores comunes evitar al crearlos.
  • React 19.2 es lo que se lanzó, no React 20 — Todavía no existe React 20. React 19.2 añade useEffectEvent, Activity, mejores seguimientos en las herramientas de desarrollo y un Suspense más fluido; merece la pena actualizar ahora mismo.
  • Deslizadores de chat que siguen flujos sin interrumpir al lector — Diseñe un MessageScroller que se mantenga cerca de la parte inferior, se congele cuando los lectores desplazan el contenido hacia arriba y ofrezca rutas de retorno claras.