Using Google Cloud Functions to Build a REST API

A REST API is a way for applications to talk to each other over the internet using standard HTTP methods while Google Cloud Functions (GCF) handles servers, provisioning, scaling, etc.
Using Google Cloud Functions to Build a REST API
Google Cloud Functions to build Rest API

A REST API (Representational State Transfer API) is basically a way for applications to talk to each other over the internet using standard HTTP methods—GET, POST, PUT, DELETE. It’s stateless, meaning every request from a client contains all the information needed to process it, and the server doesn’t store session data. This makes REST APIs fast, scalable, and flexible, perfect for building apps that perform operations like creating, reading, updating, or deleting data (CRUD).

Why Go Serverless?

Serverless computing is all about letting you focus on your code, not the servers. Google Cloud Functions (GCF) takes care of everything behind the scenes, provisioning, scaling, and maintenance so you can just write functions and deploy them.

Here’s why serverless is so attractive:

  • Pay only for what you use: No idle server costs. Your bill depends only on how often your function runs.
  • Automatic scaling: Whether you get 10 requests or 10,000, GCF scales automatically to match the demand.
  • Zero maintenance: Forget about patching, monitoring, or managing servers—Google handles all that for you.

A fintech startup, for example, cut its API costs by 70% after moving from Kubernetes to GCF. They now process 50,000 daily transactions with almost no DevOps overhead.

Google Cloud Functions vs. Alternatives:

There are other serverless options like AWS Lambda and Azure Functions, but GCF shines if you’re already in the Google Cloud ecosystem.

Feature Google Cloud Functions AWS Lambda Azure Functions
Cold Start Speed Moderate (Node.js/Python are faster) Fast (Provisioned Concurrency available) Slower (esp. C#)
Max Runtime 9 minutes 15 minutes 10 minutes
Native Triggers HTTP, Pub/Sub, Storage API Gateway, S3, SQS HTTP, Blob Storage, Event Hubs

The deep integration with Firebase, Pub/Sub, and Cloud Run makes GCF especially smooth to use, especially if you want all your services under one roof.

Understanding Serverless Architecture

Traditional application deployment often requires provisioning and maintaining virtual machines, managing Kubernetes clusters, and handling operating system updates. Google Cloud Functions (GCF) abstracts all of this, enabling developers to focus solely on writing business logic. With GCF, infrastructure concerns such as scaling, patching, and monitoring are fully managed by the platform.

How Google Cloud Functions Work

Google Cloud Functions is an event-driven Functions-as-a-Service (FaaS) platform. A deployed function executes in response to a specific event, such as an HTTP request, a message in Cloud Pub/Sub, or a change in a Cloud Storage bucket.

Core properties include:

  • Stateless execution: Each function invocation is independent. No in-memory state persists between executions, ensuring predictable behavior and simplifying scaling and recovery.
  • Ephemeral runtime: Function instances are short-lived. They are created to handle events and are shut down automatically after a period of inactivity.
  • Automatic scalability: GCF automatically provisions and deprovisions instances in real time to accommodate workload demand, eliminating the need for manual intervention.

Key Concepts In Google Cloud Functions

Triggers: Initiating Execution

Functions are activated by defined triggers. Common trigger types include:

Trigger Type Use Case
HTTP REST APIs, webhooks, and mobile backends
Cloud Pub/Sub Message processing and asynchronous workflows
Cloud Storage Reacting to file uploads, updates, or deletions
Firestore / Firebase Real-time data change handling
Cloud Scheduler Scheduled or recurring tasks (cron-style jobs)

Function Trigger Flow:

Cold Starts: Why Initial Requests Can Be Slower

A cold start occurs when a function instance is invoked after being idle. GCF must allocate resources, initialize the runtime (e.g., Node.js or Python), and load your code into memory before executing. This introduces an initial delay, commonly around 500 ms. Subsequent requests to a warm instance are significantly faster, often in the 50 ms range.

Cold vs. Warm Execution

Mitigation techniques include:

  • Provisioned concurrency to keep instances pre-warmed.
  • Warm-up requests to maintain active instances.
  • Optimized dependencies by minimizing package size and initialization complexity.

Execution Limits

Each Google Cloud Function invocation is capped at a 9-minute timeout. This makes GCF well-suited for short-lived, event-driven tasks. Workloads that require extended processing (e.g., media transcoding or ML inference) are better served by Cloud Run or Compute Engine.

Prerequisites & Setup

Before developing your serverless REST API on Google Cloud Functions (GCF), you must prepare your local development environment and configure your Google Cloud Project. This section covers the essential tools, project initialization, and a recommended folder structure for maintainable function code.

Tools Required

Ensure the following components are installed and configured before proceeding:

  • Google Cloud Account: An active Google Cloud account is required. The free tier provides sufficient usage limits for initial development and testing.
  • gcloud CLI: The Google Cloud Command Line Interface is the primary utility for managing Google Cloud resources from your terminal. It allows you to deploy functions, configure services, and manage projects. Installation instructions for all platforms are available in the official Google Cloud SDK documentation.
  • Node.js or Python: Google Cloud Functions supports multiple runtimes. This guide focuses on Node.js, but all concepts apply to Dart as well. Install a stable version of Node.js (with npm) or Dart SDK  and ensure pub commands are working on your development machine.

Initializing a Google Cloud Project

Once the required tools are available, configure your environment and prepare for deployment:

  • Authenticate the CLI:
gcloud auth login

This opens a browser window to authenticate and grant permissions.

  • Set the Active Project:
    Create or select a Google Cloud project. Configure the CLI to use your project:
gcloud config set project YOUR_PROJECT_ID

Replace YOUR_PROJECT_ID with the actual project ID.

  • Create the Project Directory:
    Create a dedicated workspace for your Cloud Function project:
mkdir my-cloud-function
cd my-cloud-function
touch index.js # For Node.js
 # OR 
touch main.dart # For Dart
  • Add the Entry Point File:
app.py #Python
main.go #Go
index.js #JavaScript

This file will hold the function logic.

A clear folder structure is crucial for scalability and maintenance as your project evolves. The following layout is recommended:

gcf-rest-api/ 
├── index.js # (Node.js) The main entry point for your Cloud Function. 
├── main.dart # (Dart) The main entry point for your Cloud Function. 
├── package.json # (Node.js) Defines project metadata and dependencies. 
├── pubspec.yaml # (Dart) Defines project metadata and dependencies.
├── pubspec.lock # (Dart) Generated by `dart pub get`.
├── .gcloudignore # Specifies files and directories to ignore during deployment. 
└── README.md # Project documentation.

Component overview:

  •  index.js / main.dart: Exports the function executed by GCF.
  •  package.json / pubspec.yaml: Lists project dependencies for automatic installation during deployment.
  • .gcloudignore: Excludes unnecessary files (e.g., node_modules/, .git/) to keep deployments lightweight.
  • README.md: Provides documentation, deployment instructions, and project details for collaborators.

Building the API

This section demonstrates how to build a REST API using Google Cloud Functions (GCF). You’ll learn how to handle HTTP triggers, define routes, manage requests and responses, and optionally integrate with backend databases. We’ll create a User resource API to illustrate essential serverless API practices.

Code Structure Overview

For larger projects, a modular file structure is essential for maintainability and scalability. The following is a recommended layout:

📂 gcf-rest-api-demo/
├── 📁 src/
│   ├── 📜 users.js    # Contains handlers for user-related CRUD operations        
│   ├── 📜 users.dart  # Contains handlers for user-related CRUD operations 
│   ├── 📜 products.js # Example: for product-related endpoints
│   └── 📜 auth.js     # Example: for authentication and JWT validation 
├── 📄 .gcloudignore   # Specifies files to ignore during deployment
├── 📄 package.json    # (Node.js) Defines project dependencies and metadata
└── 📄 pubspec.yaml    # (Dart) Defines project dependencies and metadata

HTTP Triggers and Route Definition

GCF is event-driven; for REST APIs, the event trigger is typically an HTTP request. You can either handle multiple routes in a single function or use a routing library/framework for better organization.

Node.js Implementation with Express.js

Install Express and the Functions Framework:

npm install express @google-cloud/functions-framework

Filename: index.js

// gcf-rest-api-demo/index.js (Node.js using Express for routing)
const express = require('express');
const app = express();
const functions = require('@google-cloud/functions-framework');

// Middleware to parse JSON request bodies
app.use(express.json());

// Users API routes
app.get('/api/users', (req, res) => {
  // In a real application, fetch users from a database
  res.status(200).json([{ id: 1, name: 'Jane Doe', email: 'jane@example.com' }, { id: 2, name: 'John Smith', email: 'john@example.com' }]);
});

app.post('/api/users', (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' });
  }
  // In a real application, save the new user to a database
  res.status(201).json({ id: Date.now(), name, email }); // Assign a temporary ID
});

app.put('/api/users/:id', (req, res) => {
  const userId = req.params.id;
  const updatedData = req.body;
  // In a real application, update the user in the database
  res.status(200).json({ message: `User ${userId} updated`, data: updatedData });
});

app.delete('/api/users/:id', (req, res) => {
  const userId = req.params.id;
  // In a real application, delete the user from the database
  res.status(204).send(); // 204 No Content
});

// Generic error handler (example, should be more robust)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal Server Error' });
});

// Export the Express app as the Cloud Function entry point
// The function name 'api' will be used during deployment.
functions.http('api', app);

Dart Implementation with Shelf and Shelf Router

Update your pubspec.yaml:

# gcf-rest-api-demo/pubspec.yaml
name: gcf_rest_api_dart
description: A serverless REST API built with Dart on Google Cloud Functions.
version: 1.0.0

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  functions_framework: ^0.1.0 # For Google Cloud Functions integration
  shelf: ^1.4.0              # For HTTP request handling
  shelf_router: ^1.1.0      # For defining routes
  json_annotation: ^4.9.0   # For JSON serialization/deserialization

dev_dependencies:
  build_runner: ^2.4.0
  json_serializable: ^6.7.0
  lints: ^3.0.0

Run:

dart pub get

Filename: main.dart

// gcf-rest-api-demo/main.dart (Dart using shelf_router for routing)
import 'dart:io';
import 'dart:convert';

import 'package:functions_framework/functions_framework.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:json_annotation/json_annotation.dart';

// User model for JSON serialization/deserialization
part 'main.g.dart'; // This file will be generated by build_runner

@JsonSerializable()
class User {
  int id; // Use non-final for potential updates
  String name;
  String email;

  User(this.id, this.name, this.email);

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

// Generate the part file for JSON serialization by running:
// `dart run build_runner build` in your terminal.

// Define the router for your API
final _router = Router()
  ..get('/api/users', _getUsers)
  ..post('/api/users', _createUser)
  ..put('/api/users/<id>', _updateUser)
  ..delete('/api/users/<id>', _deleteUser);

// The main HTTP Cloud Function handler.
// This function name 'api' will be used during deployment.
@CloudFunction()
Future<Response> api(Request request) async {
  // Use a pipeline to add middleware, e.g., logging
  final handler = Pipeline()
      .addMiddleware(logRequests()) // Simple request logging
      .addHandler(_router);

  return handler(request);
}

// Handler for GET /api/users
Future<Response> _getUsers(Request request) async {
  // In a real application, fetch users from a database
  final users = [
    User(1, 'Jane Doe', 'jane.doe@example.com'),
    User(2, 'John Smith', 'john.smith@example.com'),
  ];
  return Response.ok(jsonEncode(users.map((u) => u.toJson()).toList()),
      headers: {HttpHeaders.contentTypeHeader: ContentType.json.mimeType});
}

// Handler for POST /api/users
Future<Response> _createUser(Request request) async {
  try {
    final Map<String, dynamic> data = jsonDecode(await request.readAsString());
    final String? name = data['name'];
    final String? email = data['email'];

    if (name == null || email == null) {
      return Response(HttpStatus.badRequest,
          body: jsonEncode({'error': 'Name and email are required'}),
          headers: {HttpHeaders.contentTypeHeader: ContentType.json.mimeType});
    }

    // In a real application, save the new user to a database
    final newUser = User(DateTime.now().millisecondsSinceEpoch, name, email); // Assign a temporary ID
    return Response(HttpStatus.created,
        body: jsonEncode(newUser.toJson()),
        headers: {HttpHeaders.contentTypeHeader: ContentType.json.mimeType});
  } catch (e) {
    print('Error creating user: $e'); // Log the error for debugging
    return Response(HttpStatus.internalServerError,
        body: jsonEncode({'error': 'Failed to create user'}),
        headers: {HttpHeaders.contentTypeHeader: ContentType.json.mimeType});
  }
}

// Handler for PUT /api/users/:id
Future<Response> _updateUser(Request request, String id) async {
  try {
    final Map<String, dynamic> data = jsonDecode(await request.readAsString());
    // In a real application, fetch user by ID and update in the database
    // For this example, we'll just acknowledge the update
    return Response.ok(jsonEncode({'message': 'User $id updated', 'data': data}),
        headers: {HttpHeaders.contentTypeHeader: ContentType.json.mimeType});
  } catch (e) {
    print('Error updating user $id: $e');
    return Response(HttpStatus.internalServerError,
        body: jsonEncode({'error': 'Failed to update user'}),
        headers: {HttpHeaders.contentTypeHeader: ContentType.json.mimeType});
  }
}

// Handler for DELETE /api/users/:id
Future<Response> _deleteUser(Request request, String id) async {
  // In a real application, delete the user from the database
  return Response(HttpStatus.noContent); // 204 No Content
}

Request/Response Handling Best Practices:

Effective API design relies on proper handling of incoming requests and crafting appropriate responses.

  • Parsing JSON:
    • Node.js (Express): app.use(express.json()); automatically parses incoming JSON and populates req.body.
    • Dart (shelf): jsonDecode(await request.readAsString()); is used to manually read the request body as a string and then parse it into a Dart Map<String, dynamic>.
  • Setting Headers: It's good practice to explicitly set the Content-Type header for JSON responses.
    • Node.js (Express): res.json() automatically sets Content-Type: application/json. You can also use res.set('Content-Type', 'application/json');.
    • Dart (shelf): Pass headers: {HttpHeaders.contentTypeHeader: ContentType.json.mimeType} to the Response constructor.
  • HTTP Status Codes: Always use appropriate HTTP status codes to indicate the outcome of an API request.
    • 200 OK: Successful GET, PUT, or DELETE.
    • 201 Created: Successful POST.
    • 204 No Content: Successful DELETE where no content is returned.
    • 400 Bad Request: Client-side error (e.g., missing required fields).
    • 401 Unauthorized: Authentication required.
    • 403 Forbidden: Authenticated but not authorized.
    • 404 Not Found: Resource not found.
    • 405 Method Not Allowed: HTTP method not supported for the resource.
    • 500 Internal Server Error: Server-side error.
  • Error Handling: Implement robust error handling to catch unhandled exceptions and return meaningful error responses to clients.
    • Node.js (Express): Use error-handling middleware (app.use((err, req, res, next) => { ... });).

Dart (shelf): Use try-catch blocks within your route handlers to catch exceptions and return appropriate Response objects with error status codes and bodies.

Optional: Database Integration

For a truly persistent REST API, integrating with a backend database is essential. Google Cloud offers fully managed database services that seamlessly integrate with Cloud Functions.

  • Cloud Firestore (NoSQL): A flexible, scalable NoSQL document database ideal for real-time applications and hierarchical data. It offers excellent integration with Node.js via its official SDK.
  • Cloud SQL (Relational): A fully managed relational database service supporting PostgreSQL, MySQL, and SQL Server. It's suitable for applications requiring structured data with strong consistency.

Best Practices for Database Integration:

  • Connection Pooling: Always implement connection pooling for your database interactions. This minimizes the overhead of establishing new connections for each function invocation, improving performance and resource utilization.
  • Cloud SQL Auth Proxy: For secure and efficient connections to Cloud SQL, always use the Cloud SQL Auth Proxy. This utility provides authenticated and encrypted connections to your database instances without requiring public IP addresses or managing SSL certificates.
  • Environment Variables for Credentials: Never hardcode sensitive database credentials directly in your code. Instead, manage them securely using environment variables, which can be configured during function deployment.
  • SDKs and Libraries:
    • Node.js: Benefits from official Google Cloud client libraries for both Cloud Firestore and Cloud SQL, offering robust and well-maintained integration.

Dart: Typically relies on third-party community-maintained libraries (e.g., postgres for Cloud SQL) or direct utilization of Google Cloud's REST APIs for backend database access, as a fully comprehensive official Firebase Admin SDK for Dart (similar to Node.js) is not as widely available for all services.

Deployment

Deploying your Google Cloud Function (GCF) using the gcloud command-line interface is simple and efficient. This section walks you through deploying your function and managing environment variables.

Deploying via gcloud CLI

Use the gcloud functions deploy command to deploy your function. Specify the function name, runtime, and trigger type. For HTTP-triggered functions that should be publicly accessible, include the --allow-unauthenticated flag:

gcloud functions deploy api \
  --runtime nodejs20 \
  --trigger-http \
  --allow-unauthenticated

Parameters Explained:

  • api: The name of your deployed Cloud Function.
  • --runtime nodejs20: Specifies the runtime environment (Node.js 20 in this case).
  • --trigger-http: Indicates that the function responds to HTTP requests.

--allow-unauthenticated: Makes your function publicly accessible. Remove this flag if your API requires authentication.

Setting Environment Variables

Environment variables allow you to configure your application (e.g., database URLs, API keys) without hardcoding them into your code. Set them during deployment with the --set-env-vars flag:

gcloud functions deploy api \
  --set-env-vars="DB_URL=postgres://user:password@host:port/database,API_KEY=your_api_key" \
  --runtime nodejs20 \
  --trigger-http \
  --allow-unauthenticated

You can set multiple variables by separating them with commas.


Updating existing deployments:
If you’re redeploying and only want to modify environment variables while keeping other configurations intact:

gcloud functions deploy api \
  --set-env-vars "ENV=production,DATABASE_URL=..."

Important:Using --set-env-vars replaces all previously set variables. To avoid overwriting existing variables, use:

  • --update-env-vars to add or update variables
  • --remove-env-vars to remove specific variables

Conclusion

Google Cloud Functions provide a compelling and highly efficient platform for building serverless REST APIs and event-driven microservices. Their fundamental strengths lie in abstracting away infrastructure management, providing automatic scaling capabilities, and supporting a robust event-driven design. This makes them an exceptional choice for quickly developing lightweight APIs, implementing webhooks, prototyping new functionalities, and automating tasks in response to various cloud events, such as file uploads to Cloud Storage or messages published to Pub/Sub.

However, it is crucial for developers to acknowledge that for highly stateful applications, those requiring very long-lived processes, or those with extremely high throughput demands where fine-grained control over the runtime environment is paramount, alternatives like Cloud Run or Google Kubernetes Engine may offer a more appropriate, performant, and efficient solution.

By understanding both their powerful capabilities and inherent limitations, developers can effectively leverage Google Cloud Functions to build scalable, responsive, and cost-efficient serverless applications within the Google Cloud ecosystem

About the author

Dart Code Labs

Dart Code Labs - explore content on Dart backend development, authentication, microservices, and server frameworks with expert guides and insights!

Dart Code Labs

Great! You’ve successfully signed up.

Welcome back! You've successfully signed in.

You've successfully subscribed to Dart Code Labs.

Success! Check your email for magic link to sign-in.

Success! Your billing info has been updated.

Your billing was not updated.