Implementa una aplicación de Angular de pila completa en Cloud Run con Firestore usando el SDK de Admin para Node.js

Implementa una aplicación de Angular de pila completa en Cloud Run con Firestore usando el SDK de Admin para Node.js

Acerca de este codelab

subjectÚltima actualización: abr 2, 2025
account_circleEscrito por Luke Schlangen

1. Descripción general

Cloud Run es una plataforma completamente administrada que te permite ejecutar tu código directamente en la infraestructura escalable de Google. En este codelab, se mostrará cómo conectar una aplicación de Angular en Cloud Run a una base de datos de Firestore con el SDK de Admin de Node.js.

En este lab, aprenderás a hacer lo siguiente:

  • Crea una base de datos de Firestore
  • Implementa una aplicación en Cloud Run que se conecte a tu base de datos de Firestore

2. Requisitos previos

  1. Si aún no tienes una Cuenta de Google, debes crear una.
    • Usa una cuenta personal en lugar de una cuenta de trabajo o de institución educativa. Es posible que las cuentas de trabajo o de instituciones educativas tengan restricciones que te impidan habilitar las APIs necesarias para este lab.

3. Configura el proyecto

  1. Accede a la consola de Google Cloud.
  2. Habilita la facturación en la consola de Cloud.
    • Completar este lab debería costar menos de USD 1 en recursos de Cloud.
    • Puedes seguir los pasos que se indican al final de este lab para borrar recursos y evitar cargos adicionales.
    • Los usuarios nuevos son aptos para la prueba gratuita de USD 300.
  3. Crea un proyecto nuevo o elige reutilizar uno existente.

4. Abrir el editor de Cloud Shell

  1. Navega al Editor de Cloud Shell.
  2. Si la terminal no aparece en la parte inferior de la pantalla, ábrela:
    • Haz clic en el menú de opciones Ícono de menú de opciones.
    • Haz clic en Terminal.
    • Haz clic en Terminal nueva. Cómo abrir una terminal nueva en el editor de Cloud Shell
  3. En la terminal, configura tu proyecto con este comando:
    • Formato:
      gcloud config set project [PROJECT_ID]
    • Ejemplo:
      gcloud config set project lab-project-id-example
    • Si no recuerdas el ID de tu proyecto, haz lo siguiente:
      • Puedes enumerar todos los IDs de tu proyecto con lo siguiente:
        gcloud projects list | awk '/PROJECT_ID/{print $2}'
      Establece el ID del proyecto en la terminal del Editor de Cloud Shell
  4. Si se te solicita autorización, haz clic en Autorizar para continuar. Haz clic para autorizar Cloud Shell
  5. Deberías ver el siguiente mensaje:
    Updated property [core/project].
    
    Si ves un WARNING y se te pregunta Do you want to continue (Y/N)?, es probable que hayas ingresado el ID del proyecto de forma incorrecta. Presiona N, presiona Enter y vuelve a intentar ejecutar el comando gcloud config set project.

5. Habilita las APIs

En la terminal, habilita las APIs:

gcloud services enable \
  firestore.googleapis.com \
  run.googleapis.com \
  artifactregistry.googleapis.com \
  cloudbuild.googleapis.com

Si se te solicita autorización, haz clic en Autorizar para continuar. Haz clic para autorizar Cloud Shell

Este comando puede tardar unos minutos en completarse, pero, en algún momento, debería mostrar un mensaje de éxito similar al siguiente:

Operation "operations/acf.p2-73d90d00-47ee-447a-b600" finished successfully.

6. Crea una base de datos de Firestore

  1. Ejecuta el comando gcloud firestore databases create para crear una base de datos de Firestore
    gcloud firestore databases create --location=nam5

7. Prepara la solicitud

Prepara una aplicación de Next.js que responda a las solicitudes HTTP.

  1. Para crear un proyecto de Next.js nuevo llamado task-app, usa el siguiente comando:
    npx --yes @angular/cli@19.2.5 new task-app \
        --minimal \
        --inline-template \
        --inline-style \
        --ssr \
        --server-routing \
        --defaults
  2. Cambia el directorio a task-app:
    cd task-app
  1. Instala firebase-admin para interactuar con la base de datos de Firestore.
    npm install firebase-admin
  1. Abre el archivo server.ts en el editor de código de Cloud Shell:
    cloudshell edit src/server.ts
    Ahora debería aparecer un archivo en la parte superior de la pantalla. Aquí es donde puedes editar el archivo server.ts. Muestra que el código va en la sección superior de la pantalla
  2. Borra el contenido existente del archivo server.ts.
  3. Copia el siguiente código y pégalo en el archivo server.ts abierto:
    import {
      AngularNodeAppEngine,
      createNodeRequestHandler,
      isMainModule,
      writeResponseToNodeResponse,
    } from '@angular/ssr/node';
    import express from 'express';
    import { dirname, resolve } from 'node:path';
    import { fileURLToPath } from 'node:url';
    import { initializeApp, applicationDefault, getApps } from 'firebase-admin/app';
    import { getFirestore } from 'firebase-admin/firestore';

    type Task = {
      id: string;
      title: string;
      status: 'IN_PROGRESS' | 'COMPLETE';
      createdAt: number;
    };

    const credential = applicationDefault();

    // Only initialize app if it does not already exist
    if (getApps().length === 0) {
      initializeApp({ credential });
    }

    const db = getFirestore();
    const tasksRef = db.collection('tasks');

    const serverDistFolder = dirname(fileURLToPath(import.meta.url));
    const browserDistFolder = resolve(serverDistFolder, '../browser');

    const app = express();
    const angularApp = new AngularNodeAppEngine();

    app.use(express.json());

    app.get('/api/tasks', async (req, res) => {
      const snapshot = await tasksRef.orderBy('createdAt', 'desc').limit(100).get();
      const tasks: Task[] = snapshot.docs.map(doc => ({
        id: doc.id,
        title: doc.data()['title'],
        status: doc.data()['status'],
        createdAt: doc.data()['createdAt'],
      }));
      res.send(tasks);
    });

    app.post('/api/tasks', async (req, res) => {
      const newTaskTitle = req.body.title;
      if(!newTaskTitle){
        res.status(400).send("Title is required");
        return;
      }
      await tasksRef.doc().create({
        title: newTaskTitle,
        status: 'IN_PROGRESS',
        createdAt: Date.now(),
      });
      res.sendStatus(200);
    });

    app.put('/api/tasks', async (req, res) => {
      const task: Task = req.body;
      if (!task || !task.id || !task.title || !task.status) {
        res.status(400).send("Invalid task data");
        return;
      }
      await tasksRef.doc(task.id).set(task);
      res.sendStatus(200);
    });

    app.delete('/api/tasks', async (req, res) => {
      const task: Task = req.body;
      if(!task || !task.id){
        res.status(400).send("Task ID is required");
        return;
      }
      await tasksRef.doc(task.id).delete();
      res.sendStatus(200);
    });

    /**
    * Serve static files from /browser
    */
    app.use(
      express.static(browserDistFolder, {
        maxAge: '1y',
        index: false,
        redirect: false,
      }),
    );

    /**
    * Handle all other requests by rendering the Angular application.
    */
    app.use('/**', (req, res, next) => {
      angularApp
        .handle(req)
        .then((response) =>
          response ? writeResponseToNodeResponse(response, res) : next(),
        )
        .catch(next);
    });

    /**
    * Start the server if this module is the main entry point.
    * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
    */
    if (isMainModule(import.meta.url)) {
      const port = process.env['PORT'] || 4000;
      app.listen(port, () => {
        console.log(`Node Express server listening on http://localhost:${port}`);
      });
    }

    /**
    * Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions.
    */
    export const reqHandler = createNodeRequestHandler(app);
  4. Abre el archivo angular.json en el editor de código de Cloud Shell:
    cloudshell edit angular.json
    Ahora agregaremos la línea "externalDependencies": ["firebase-admin"] al archivo angular.json.
  5. Borra el contenido existente del archivo angular.json.
  6. Copia el siguiente código y pégalo en el archivo angular.json abierto:
    {
      "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
      "version": 1,
      "newProjectRoot": "projects",
      "projects": {
        "task-app": {
          "projectType": "application",
          "schematics": {
            "@schematics/angular:component": {
              "inlineTemplate": true,
              "inlineStyle": true,
              "skipTests": true
            },
            "@schematics/angular:class": {
              "skipTests": true
            },
            "@schematics/angular:directive": {
              "skipTests": true
            },
            "@schematics/angular:guard": {
              "skipTests": true
            },
            "@schematics/angular:interceptor": {
              "skipTests": true
            },
            "@schematics/angular:pipe": {
              "skipTests": true
            },
            "@schematics/angular:resolver": {
              "skipTests": true
            },
            "@schematics/angular:service": {
              "skipTests": true
            }
          },
          "root": "",
          "sourceRoot": "src",
          "prefix": "app",
          "architect": {
            "build": {
              "builder": "@angular-devkit/build-angular:application",
              "options": {
                "outputPath": "dist/task-app",
                "index": "src/index.html",
                "browser": "src/main.ts",
                "polyfills": [
                  "zone.js"
                ],
                "tsConfig": "tsconfig.app.json",
                "assets": [
                  {
                    "glob": "**/*",
                    "input": "public"
                  }
                ],
                "styles": [
                  "src/styles.css"
                ],
                "scripts": [],
                "server": "src/main.server.ts",
                "outputMode": "server",
                "ssr": {
                  "entry": "src/server.ts"
                },
                "externalDependencies": ["firebase-admin"]
              },
              "configurations": {
                "production": {
                  "budgets": [
                    {
                      "type": "initial",
                      "maximumWarning": "500kB",
                      "maximumError": "1MB"
                    },
                    {
                      "type": "anyComponentStyle",
                      "maximumWarning": "4kB",
                      "maximumError": "8kB"
                    }
                  ],
                  "outputHashing": "all"
                },
                "development": {
                  "optimization": false,
                  "extractLicenses": false,
                  "sourceMap": true
                }
              },
              "defaultConfiguration": "production"
            },
            "serve": {
              "builder": "@angular-devkit/build-angular:dev-server",
              "configurations": {
                "production": {
                  "buildTarget": "task-app:build:production"
                },
                "development": {
                  "buildTarget": "task-app:build:development"
                }
              },
              "defaultConfiguration": "development"
            },
            "extract-i18n": {
              "builder": "@angular-devkit/build-angular:extract-i18n"
            }
          }
        }
      }
    }

"externalDependencies": ["firebase-admin"]

  1. Abre el archivo app.component.ts en el editor de código de Cloud Shell:
    cloudshell edit src/app/app.component.ts
    Ahora debería aparecer un archivo existente en la parte superior de la pantalla. Aquí es donde puedes editar el archivo app.component.ts. Muestra que el código va en la sección superior de la pantalla
  2. Borra el contenido existente del archivo app.component.ts.
  3. Copia el siguiente código y pégalo en el archivo app.component.ts abierto:
    import { afterNextRender, Component, signal } from '@angular/core';
    import { FormsModule } from '@angular/forms';

    type Task = {
      id: string;
      title: string;
      status: 'IN_PROGRESS' | 'COMPLETE';
      createdAt: number;
    };

    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [FormsModule],
      template: `
        <section>
          <input
            type="text"
            placeholder="New Task Title"
            [(ngModel)]="newTaskTitle"
            class="text-black border-2 p-2 m-2 rounded"
          />
          <button (click)="addTask()">Add new task</button>
          <table>
            <tbody>
              @for (task of tasks(); track task) {
                @let isComplete = task.status === 'COMPLETE';
                <tr>
                  <td>
                    <input
                      (click)="updateTask(task, { status: isComplete ? 'IN_PROGRESS' : 'COMPLETE' })"
                      type="checkbox"
                      [checked]="isComplete"
                    />
                  </td>
                  <td>{{ task.title }}</td>
                  <td>{{ task.status }}</td>
                  <td>
                    <button (click)="deleteTask(task)">Delete</button>
                  </td>
                </tr>
              }
            </tbody>
          </table>
        </section>
      `,
      styles: '',
    })
    export class AppComponent {
      newTaskTitle = '';
      tasks = signal<Task[]>([]);

      constructor() {
        afterNextRender({
          earlyRead: () => this.getTasks()
        });
      }

      async getTasks() {
        const response = await fetch(`/api/tasks`);
        const tasks = await response.json();
        this.tasks.set(tasks);
      }

      async addTask() {
        await fetch(`/api/tasks`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            title: this.newTaskTitle,
            status: 'IN_PROGRESS',
            createdAt: Date.now(),
          }),
        });
        this.newTaskTitle = '';
        await this.getTasks();
      }

      async updateTask(task: Task, newTaskValues: Partial<Task>) {
        await fetch(`/api/tasks`, {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ ...task, ...newTaskValues }),
        });
        await this.getTasks();
      }

      async deleteTask(task: any) {
        await fetch('/api/tasks', {
          method: 'DELETE',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(task),
        });
        await this.getTasks();
      }
    }

La aplicación ya está lista para implementarse.

8. Implementa la aplicación en Cloud Run

  1. Ejecuta el siguiente comando para implementar tu aplicación en Cloud Run:
    gcloud run deploy helloworld \
      --region=us-central1 \
      --source=.
  2. Si se te solicita, presiona Y y Enter para confirmar que deseas continuar:
    Do you want to continue (Y/n)? Y
    

Después de unos minutos, la aplicación debería proporcionarte una URL para que la visites.

Navega a la URL para ver tu aplicación en acción. Cada vez que visites la URL o actualices la página, verás la app de tareas.

9. Felicitaciones

En este lab, aprendiste a hacer las siguientes tareas:

  • Crea una instancia de Cloud SQL para PostgreSQL
  • Implementa una aplicación en Cloud Run que se conecte a tu base de datos de Cloud SQL

Limpia

Cloud SQL no tiene un nivel gratuito y se te cobrará si continúas usándolo. Puedes borrar tu proyecto de Cloud para evitar que se generen cargos adicionales.

Si bien Cloud Run no cobra cuando el servicio no se usa, es posible que se te cobre por el almacenamiento de la imagen del contenedor en Artifact Registry. Si borras tu proyecto de Cloud, se detendrá la facturación de todos los recursos que usaste en ese proyecto.

Si lo deseas, borra el proyecto:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

También te recomendamos que borres los recursos innecesarios del disco de CloudShell. Puedes hacer lo siguiente:

  1. Borra el directorio del proyecto del codelab:
    rm -rf ~/task-app
  2. Advertencia. No podrás deshacer esta acción. Si quieres borrar todo en Cloud Shell para liberar espacio, puedes borrar todo el directorio principal. Asegúrate de que todo lo que quieras conservar esté guardado en otro lugar.
    sudo rm -rf $HOME