Google Sheets as a REST API in Angular
Use SheetsAPI to turn a Google Sheet into a type-safe Angular service with RxJS Observables, the HttpClient, error handling, and real-time search.
Angular's HttpClient and RxJS make it a great fit for working with REST APIs that
return plain JSON - and that is exactly what SheetsAPI delivers. This guide walks you
through building a typed Angular service backed by a live Google Sheet.
Prerequisites
- An Angular 17+ project (
ng new my-app --standalone) - A Google Sheet with at least a header row
- A SheetsAPI account and a
YOUR_USER_KEYvalue from the dashboard
1. Enable HttpClient
Register the client in app.config.ts:
import { ApplicationConfig } from "@angular/core";
import { provideHttpClient, withFetch } from "@angular/common/http";
export const appConfig: ApplicationConfig = {
providers: [provideHttpClient(withFetch())],
};2. Define the response types
// src/app/models/sheet.model.ts
export interface SheetMeta {
total: number;
limit: number;
offset: number;
}
export interface SheetResponse<T> {
data: T[];
meta: SheetMeta;
}3. Create a generic SheetsAPI service
// src/app/services/sheets.service.ts
import { Injectable, inject } from "@angular/core";
import { HttpClient, HttpParams } from "@angular/common/http";
import { Observable } from "rxjs";
import { SheetResponse } from "../models/sheet.model";
export interface SheetQueryOptions {
limit?: number;
offset?: number;
search?: string;
sort?: string;
fields?: string;
}
const BASE = "https://sheetsapi.gkit.mreshank.com/api/spreadsheets";
@Injectable({ providedIn: "root" })
export class SheetsService {
private http = inject(HttpClient);
query<T>(
userKey: string,
sheetName: string,
opts: SheetQueryOptions = {},
): Observable<SheetResponse<T>> {
let params = new HttpParams();
if (opts.limit !== undefined) params = params.set("limit", opts.limit);
if (opts.offset !== undefined) params = params.set("offset", opts.offset);
if (opts.search) params = params.set("search", opts.search);
if (opts.sort) params = params.set("sort", opts.sort);
if (opts.fields) params = params.set("fields", opts.fields);
return this.http.get<SheetResponse<T>>(`${BASE}/YOUR_USER_KEY/${sheetName}`, { params });
}
}Replace YOUR_USER_KEY with your actual key.
4. Use the service in a component
// src/app/components/products/products.component.ts
import { Component, OnInit, inject, signal, computed } from "@angular/core";
import { AsyncPipe, NgFor, NgIf } from "@angular/common";
import { SheetsService } from "../../services/sheets.service";
import { Observable } from "rxjs";
interface Product {
name: string;
price: string;
category: string;
stock: string;
}
@Component({
selector: "app-products",
standalone: true,
imports: [AsyncPipe, NgFor, NgIf],
template: `
<input placeholder="Search products…" (input)="onSearch($event)" />
<ng-container *ngIf="products$ | async as resp">
<p>{{ resp.meta.total }} products</p>
<ul>
<li *ngFor="let p of resp.data">{{ p.name }} - {{ p.price }} ({{ p.category }})</li>
</ul>
</ng-container>
`,
})
export class ProductsComponent implements OnInit {
private sheets = inject(SheetsService);
products$!: Observable<{
data: Product[];
meta: { total: number; limit: number; offset: number };
}>;
ngOnInit() {
this.load();
}
load(search = "") {
this.products$ = this.sheets.query<Product>("YOUR_USER_KEY", "Products", {
limit: 20,
sort: "name",
...(search ? { search: `name:${search}` } : {}),
});
}
onSearch(event: Event) {
const q = (event.target as HTMLInputElement).value;
this.load(q);
}
}5. Add an interceptor for private sheets
If your sheet requires authentication, attach a Bearer token with an interceptor:
// src/app/interceptors/auth.interceptor.ts
import { HttpInterceptorFn } from "@angular/common/http";
const API_KEY = "sk_your_api_key_here";
export const authInterceptor: HttpInterceptorFn = (req, next) => {
if (req.url.includes("sheetsapi.gkit.mreshank.com")) {
const authReq = req.clone({
setHeaders: { Authorization: `Bearer ${API_KEY}` },
});
return next(authReq);
}
return next(req);
};Register it in app.config.ts:
import { provideHttpClient, withFetch, withInterceptors } from "@angular/common/http";
import { authInterceptor } from "./interceptors/auth.interceptor";
providers: [provideHttpClient(withFetch(), withInterceptors([authInterceptor]))];6. Reactive search with switchMap
Avoid a new subscription on every keystroke:
import { Component, inject } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { debounceTime, distinctUntilChanged, switchMap, startWith } from "rxjs/operators";
import { SheetsService } from "../../services/sheets.service";
@Component({
standalone: true,
imports: [ReactiveFormsModule, AsyncPipe, NgFor],
template: `
<input [formControl]="searchCtrl" placeholder="Search…" />
<ul>
<li *ngFor="let item of results$ | async | keyvalue"></li>
</ul>
`,
})
export class ReactiveSearchComponent {
private sheets = inject(SheetsService);
searchCtrl = new FormControl("");
results$ = this.searchCtrl.valueChanges.pipe(
startWith(""),
debounceTime(300),
distinctUntilChanged(),
switchMap((q) =>
this.sheets.query("YOUR_USER_KEY", "Products", {
...(q ? { search: `name:${q}` } : {}),
}),
),
);
}switchMap cancels the previous HTTP request automatically if the user types again
before the response arrives - the right default for search inputs.
7. Server-side rendering (Angular Universal)
If you use @angular/ssr, the HttpClient with withFetch() works in both browser
and Node.js contexts without any extra configuration. SheetsAPI responses include
standard Cache-Control headers, so Angular's TransferState caching works out of
the box.
Summary
| Concept | Implementation |
|---|---|
| HTTP calls | HttpClient.get<T>() |
| Type safety | Generic SheetResponse<T> |
| Auth | HttpInterceptorFn adds Bearer token |
| Reactive search | switchMap + debounceTime |
| SSR | Works via withFetch() |
SheetsAPI turns Google Sheets into a production-grade JSON endpoint - your Angular app just treats it as any other REST API.