Introduction
Google Cloud Spanner is a fully managed relational database that scales horizontally at global level, with full ACID compliance and strong consistency. Unlike traditional databases like PostgreSQL that struggle beyond a few TB, Spanner handles petabytes without downtime—ideal for fintech, e-commerce, or IoT apps needing high availability.
This beginner tutorial guides you step by step: from installing the SDK to running SQL queries via Node.js. You'll learn to create an instance, a database, tables, insert data, and query in real time. By the end, you'll have a fully functional project you can bookmark. Allow 30-45 minutes and a free GCP account (300$ credit). Why 2026? New features like Gemini integration for AI-powered queries and optimized developer pricing.
Key benefits: Global latency under 10ms, auto-scaling, automated backups. Perfect for replacing Cassandra or BigQuery in OLTP workloads.
Prerequisites
- Active Google Cloud Platform (GCP) account with billing enabled (300$ free credit).
- Node.js 20+ installed.
- gcloud CLI version 450+.
- Basic TypeScript/JavaScript and SQL knowledge.
- Code editor (VS Code recommended).
Enable the API and Install the SDK
# Authenticate gcloud
gcloud auth login
gcloud config set project YOUR-PROJECT-ID # Replace with your GCP project ID
gcloud services enable spanner.googleapis.com
gcloud components install beta
# Install the Node.js client
mkdir spanner-tutorial && cd spanner-tutorial
npm init -y
npm install @google-cloud/spanner typescript @types/node ts-node
# Compile TS (optional)
npm install -D typescriptThis script enables the Spanner API, authenticates gcloud, and sets up a Node.js project with the official client. Replace YOUR-PROJECT-ID with your GCP project ID (found in console.cloud.google.com). Common pitfalls: forgetting auth or project ID leads to 403/404 errors.
Set Up the Environment
Once you've run the bash setup, verify with gcloud projects list. Download service account credentials if needed (IAM > Service Accounts > Create JSON key), but gcloud auth works fine for local dev. Create a .env file for IDs: PROJECT_ID=yourproject, INSTANCE_ID=test-instance, DATABASE_ID=test-db. Spanner is multi-regional by default for high availability.
Create the Spanner Instance
# Create a regional instance (config=1 for 1000 processing units, adjust for prod)
gcloud spanner instances create $INSTANCE_ID \
--config=nam-eur3 \
--description="Tutorial instance" \
--nodes=1
# Verify
sleep 60
gcloud spanner instances describe $INSTANCE_ID
# Environment variables (add to .env)
export INSTANCE_ID=test-instanceCreates a minimal instance (1 node, ~$100/month, free in dev). nam-eur3 is Europe; list configs with gcloud spanner instance-configs list. Pitfall: Provisioning takes 1-2 minutes; skipping the wait causes timeouts.
Create the Database
The instance is the scalable container; the database lives inside it logically. Use DDL SQL for schemas. Think of Spanner as SQL + NoSQL: interleaved tables for hierarchies (e.g., users > orders).
Create the Database and Tables
# Create the DB
gcloud spanner databases create $DATABASE_ID --instance=$INSTANCE_ID --ddl='CREATE TABLE Users (
UserId INT64 NOT NULL,
Email STRING(256) NOT NULL,
Name STRING(100),
) PRIMARY KEY(UserId)' \
--ddl='CREATE TABLE Orders (
OrderId INT64 NOT NULL,
UserId INT64 NOT NULL,
Product STRING(100),
Amount FLOAT64,
) PRIMARY KEY(OrderId),
INTERLEAVE IN PARENT Users ON DELETE CASCADE'
# Verify
databases list --instance=$INSTANCE_IDCreates a DB with Users and Orders tables. Interleave links Orders to Users for fast, low-latency joins. Use FLOAT64 for amounts; NOT NULL avoids nulls. Pitfall: Strict DDL syntax—test in the Spanner console first.
Node.js Client for Insertions
Analogy: Like a database driver, the client handles sessions and transactions. Spanner supports batch mutations for top performance.
Insert Data
import { Spanner } from '@google-cloud/spanner';
const spanner = new Spanner({
projectId: process.env.PROJECT_ID!,
});
const instance = spanner.instance(process.env.INSTANCE_ID!);
const database = instance.database(process.env.DATABASE_ID!);
async function insertData() {
const tx = await database.runTransaction();
try {
await tx.insert('Users', {
UserId: 1n,
Email: 'alice@example.com',
Name: 'Alice',
});
await tx.insert('Orders', {
OrderId: 101n,
UserId: 1n,
Product: 'Laptop',
Amount: 999.99,
});
await tx.commit();
console.log('Données insérées');
} catch (err) {
await tx.rollback();
console.error(err);
}
}
insertData();Inserts data in an ACID transaction. Use BigInt (1n) for INT64. RunTransaction ensures consistency. Pitfall: Forgetting commit/rollback causes crashes; always use async/await.
Run Queries
Standard SQL queries plus Spanner extensions (ARRAY, STRUCT). Use pagination with LIMIT/OFFSET for massive scale.
Read and Query Data
import { Spanner } from '@google-cloud/spanner';
const spanner = new Spanner({ projectId: process.env.PROJECT_ID! });
const instance = spanner.instance(process.env.INSTANCE_ID!);
const database = instance.database(process.env.DATABASE_ID!);
async function queryData() {
const [[usersRow]] = await database.run({
sql: `SELECT u.UserId, u.Name, ARRAY_AGG(o.Product) AS Products
FROM Users u
LEFT JOIN Orders o ON u.UserId = o.UserId
GROUP BY u.UserId, u.Name`,
});
console.log('Users et orders:', usersRow);
// Single row
const [order] = await database.run({
sql: 'SELECT * FROM Orders WHERE OrderId = @id',
params: { id: 101n },
types: { id: 'INT64' },
});
console.log('Order:', order);
}
queryData();JOIN query with ARRAY_AGG (Spanner-specific). Params and types prevent SQL injection. Pitfall: Types are required for params; omitting them causes parsing errors.
Cleanup and Full Script
import { Spanner } from '@google-cloud/spanner';
import dotenv from 'dotenv';
dotenv.config();
const spanner = new Spanner({ projectId: process.env.PROJECT_ID! });
const instance = spanner.instance(process.env.INSTANCE_ID!);
const database = instance.database(process.env.DATABASE_ID!);
async function main() {
// Insert
const tx = await database.runTransaction();
try {
await tx.insert('Users', { UserId: 2n, Email: 'bob@example.com', Name: 'Bob' });
await tx.insert('Orders', { OrderId: 102n, UserId: 2n, Product: 'Phone', Amount: 599.99 });
await tx.commit();
} catch (err) {
await tx.rollback();
console.error('Insert failed:', err);
return;
}
// Query
const [rows] = await database.run({
sql: 'SELECT * FROM Users u JOIN Orders o ON u.UserId = o.UserId',
});
console.table(rows);
}
main().catch(console.error);
// Run: ts-node full-app.tsAll-in-one script: dotenv for env vars, insert + query. console.table for visualization. Pitfall: .env ignored without dotenv; always add global catch.
Best Practices
- Always use transactions: Guarantees ACID for multi-mutations.
- Interleave tables: For 1:N relationships, reduces join latency 10x.
- Secondary indexes:
CREATE INDEX ON Orders(UserId)for fast queries. - Monitoring: Use Cloud Monitoring for CPU/QPS; auto-scale nodes.
- Automated backups: Point-in-time recovery up to 7 days, free.
Common Errors to Avoid
- Missing param types: Queries crash with 'Invalid type'. Always specify
{ id: 'INT64' }. - No provisioning wait: Instance/DB takes 2-5 min; poll with describe.
- Quota exceeded: 1 free instance; upgrade for prod.
- INT64 as number: Use BigInt—JS numbers overflow >2^53.
Next Steps
- Official docs: Cloud Spanner Node.js.
- Advanced: ReadWriteTransactions, Backup/Restore.
- Check out our Learni Google Cloud training for CKAD/Spanner expert certs.
- Open-source project: Clone this tutorial on GitHub and fork for your apps.