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.
GKit is currently undergoing platform maintenance
Be the first to know when all Google Workspace tools are back online, plus get our engineering deep-dives and product announcements directly to your inbox.