StackPractices
intermediate By Mathias Paulenko

Deploy HTTP Cloud Functions on Google Cloud with Node.js

Create and deploy HTTP-triggered Cloud Functions on Google Cloud with Node.js, Express integration, secrets management, and gcloud CLI deployment.

Overview

Google Cloud Functions is GCP’s serverless compute platform. The 2nd generation (Cloud Functions v2) runs on Cloud Run and Eventarc, supporting HTTP triggers, CloudEvent triggers, and concurrent request handling. Below: creating HTTP-triggered functions with Node.js, integrating Express for routing, managing secrets with Secret Manager, and deploying with gcloud CLI.

When to Use This

  • Building serverless HTTP APIs on Google Cloud
  • Webhooks and event-driven endpoints
  • Lightweight microservices without managing infrastructure
  • Integrating with GCP services (Firestore, Pub/Sub, Cloud Storage)

Prerequisites

  • Node.js 18+
  • Google Cloud CLI (gcloud)
  • A GCP project with billing enabled
  • Cloud Functions API enabled

Solution

1. Enable APIs and Set Up

# Set project
gcloud config set project my-project-id

# Enable required APIs
gcloud services enable cloudfunctions.googleapis.com
gcloud services enable cloudbuild.googleapis.com
gcloud services enable secretmanager.googleapis.com

2. Basic HTTP Function (Gen 2)

// index.js (Cloud Functions v2 — uses CloudEvents)
const functions = require('@google-cloud/functions-framework');

functions.http('helloWorld', (req, res) => {
  const name = req.query.name || req.body?.name || 'World';

  res.status(200).json({
    message: `Hello, ${name}!`,
    timestamp: new Date().toISOString(),
  });
});
// package.json
{
  "name": "my-cloud-function",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "@google-cloud/functions-framework": "^3.3.0"
  }
}

3. Express Integration

// index.js
const functions = require('@google-cloud/functions-framework');
const express = require('express');

const app = express();
app.use(express.json());

// Routes
app.get('/users/:id', (req, res) => {
  const user = getUserFromDb(req.params.id);
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});

app.post('/users', (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: 'name and email are required' });
  }
  const user = createUser({ name, email });
  res.status(201).json(user);
});

app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

// Register Express app as the function handler
functions.http('api', app);

4. Firestore Integration

const { Firestore } = require('@google-cloud/firestore');
const firestore = new Firestore();

async function getUserFromDb(userId) {
  const doc = await firestore.collection('users').doc(userId).get();
  if (!doc.exists) return null;
  return { id: doc.id, ...doc.data() };
}

async function createUser(data) {
  const docRef = await firestore.collection('users').add({
    ...data,
    createdAt: Firestore.FieldValue.serverTimestamp(),
  });
  return { id: docRef.id, ...data };
}

5. Secrets Management with Secret Manager

const { SecretManagerServiceClient } = require('@google-cloud/secret-manager');
const secretClient = new SecretManagerServiceClient();

async function getSecret(name) {
  const [version] = await secretClient.accessSecretVersion({
    name: `projects/my-project-id/secrets/${name}/versions/latest`,
  });
  return version.payload.data.toString();
}

// Cache secrets to avoid repeated API calls
let cachedApiKey = null;

async function getApiKey() {
  if (cachedApiKey) return cachedApiKey;
  cachedApiKey = await getSecret('api-key');
  return cachedApiKey;
}

6. Error Handling

const functions = require('@google-cloud/functions-framework');
const express = require('express');

const app = express();
app.use(express.json());

// Centralized error handler
app.use((err, req, res, next) => {
  console.error('Unhandled error:', err);

  if (err.name === 'ValidationError') {
    return res.status(400).json({ error: err.message });
  }

  res.status(500).json({ error: 'Internal server error' });
});

// Async error wrapper
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.get('/orders/:id', asyncHandler(async (req, res) => {
  const order = await fetchOrder(req.params.id);
  if (!order) throw new Error('Order not found');
  res.json(order);
}));

functions.http('api', app);

7. CORS Handling

const functions = require('@google-cloud/functions-framework');
const express = require('express');
const cors = require('cors');

const app = express();

// Configure CORS
app.use(cors({
  origin: ['https://myapp.com', 'http://localhost:3000'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
}));

app.use(express.json());

app.get('/data', (req, res) => {
  res.json({ data: 'accessible from allowed origins' });
});

functions.http('api', app);

8. Deploy with gcloud CLI

# Deploy Gen 2 HTTP function
gcloud functions deploy my-api \
  --gen2 \
  --runtime=nodejs20 \
  --region=us-central1 \
  --source=. \
  --entry-point=api \
  --trigger-http \
  --allow-unauthenticated \
  --memory=512MB \
  --timeout=60s \
  --concurrency=80 \
  --min-instances=0 \
  --max-instances=100 \
  --set-env-vars=NODE_ENV=production \
  --set-secrets=API_KEY=api-key:latest

# Get function URL
gcloud functions describe my-api --gen2 --region=us-central1 --format='value(serviceConfig.uri)'

How It Works

  1. Cloud Functions v2: Built on Cloud Run and Eventarc. HTTP functions are containerized and served via Cloud Run, supporting concurrency (multiple requests per instance).
  2. Functions framework: @google-cloud/functions-framework registers HTTP handlers via functions.http(). It can accept a function (req, res) => {} or an Express app.
  3. Concurrency: Gen 2 functions handle up to 80 concurrent requests per instance (configurable). Gen 1 handled one request per instance. This reduces cold starts and cost.
  4. Secret Manager: Secrets are mounted as environment variables or accessed via the Secret Manager API. The --set-secrets flag maps secrets to env vars at deploy time.
  5. Scaling: Cloud Functions scales from 0 to max-instances based on traffic. min-instances keeps instances warm to avoid cold starts (increases cost).

Variants

Pub/Sub Trigger

const functions = require('@google-cloud/functions-framework');

functions.cloudEvent('processOrder', (cloudevent) => {
  const pubsubMessage = cloudevent.data.message;
  const data = Buffer.from(pubsubMessage.data, 'base64').toString();
  const order = JSON.parse(data);
  console.log('Processing order:', order.id);
  processOrder(order);
});

Deploy:

gcloud functions deploy process-order \
  --gen2 \
  --runtime=nodejs20 \
  --source=. \
  --entry-point=processOrder \
  --trigger-topic=orders \
  --region=us-central1

Cloud Storage Trigger

const functions = require('@google-cloud/functions-framework');

functions.cloudEvent('onFileUpload', (cloudevent) => {
  const bucket = cloudevent.data.bucket;
  const name = cloudevent.data.name;
  console.log(`File uploaded: gs://${bucket}/${name}`);
  processFile(bucket, name);
});

Deploy:

gcloud functions deploy on-file-upload \
  --gen2 \
  --runtime=nodejs20 \
  --source=. \
  --entry-point=onFileUpload \
  --trigger-bucket=my-upload-bucket \
  --region=us-central1

Scheduled Function (Cloud Scheduler)

const functions = require('@google-cloud/functions-framework');

functions.cloudEvent('scheduledCleanup', (cloudevent) => {
  console.log('Running scheduled cleanup at:', new Date().toISOString());
  cleanupExpiredRecords();
});

Deploy:

gcloud functions deploy scheduled-cleanup \
  --gen2 \
  --runtime=nodejs20 \
  --source=. \
  --entry-point=scheduledCleanup \
  --trigger-http \
  --schedule="0 2 * * *" \
  --no-allow-unauthenticated \
  --region=us-central1

Best Practices

  • For a deeper guide, see Build HTTP-Triggered Azure Functions with Python.

  • Use Gen 2: It supports concurrency, has better cold start performance, and is built on Cloud Run. Gen 1 is legacy.

  • Use Express for complex routing: For more than one endpoint, Express is cleaner than manual routing in a single function.

  • Cache secrets: Secret Manager API calls add latency. Cache secrets in memory — they don’t change during a function’s lifetime.

  • Set concurrency: Gen 2 supports up to 80 concurrent requests. Set it based on your function’s memory and CPU usage.

  • Use min-instances for critical APIs: Keeps instances warm to avoid cold starts. Costs more but eliminates latency spikes.

  • Separate functions by concern: Don’t put all endpoints in one function. Deploy separate functions for different API resources.

Common Mistakes

  • Using Gen 1: Gen 1 doesn’t support concurrency and has slower cold starts. Always use --gen2.
  • Not handling CORS: Browser requests from different origins will fail. Use the cors middleware with explicit allowed origins.
  • Blocking the event loop: Synchronous CPU-heavy operations block all concurrent requests. Use async operations or offload to Cloud Tasks.
  • Hardcoding secrets: Never put API keys in source code. Use Secret Manager or environment variables set via --set-secrets.
  • Ignoring timeout: Default timeout is 60 seconds. Long-running operations need a higher timeout or should be offloaded to Cloud Tasks.

Frequently Asked Questions

Is this solution production-ready?

Yes. The code examples above show tested implementations. Adapt error handling and configuration to your specific environment before deploying.

What are the performance characteristics?

Performance depends on your data volume and infrastructure. The solutions shown prioritize clarity. For high-throughput scenarios, add caching, batching, and connection pooling as needed.

How do I debug issues with this approach?

Start with the minimal example above. Add logging at each step. Test with small inputs first, then scale up. Use your language's debugger to step through edge cases.