├── bun.lockb ├── src ├── app │ ├── favicon.ico │ ├── api │ │ ├── route.ts │ │ └── v1 │ │ │ └── sheets │ │ │ ├── route.ts │ │ │ └── [spreadsheet_id] │ │ │ ├── route.ts │ │ │ └── [sheet_name] │ │ │ ├── [rows] │ │ │ └── route.ts │ │ │ └── route.ts │ ├── docs │ │ ├── md.module.css │ │ └── page.tsx │ ├── globals.css │ ├── layout.tsx │ ├── example │ │ ├── TodoItem.tsx │ │ ├── actions.tsx │ │ ├── AddNewTodo.tsx │ │ └── page.tsx │ └── page.tsx ├── components │ ├── theme-provider.tsx │ ├── theme-switcher.tsx │ └── spinner.tsx └── utils │ ├── utils.ts │ └── sheets.ts ├── public ├── vercel.svg ├── openapi.json └── gsheet-rest-api.postman_collection.json ├── next.config.ts ├── postcss.config.mjs ├── env.example ├── .prettierrc.mjs ├── eslint.config.mjs ├── .gitignore ├── tsconfig.json ├── LICENSE ├── package.json ├── middleware.ts └── README.md /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mazipan/gsheet-rest-api/HEAD/bun.lockb -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mazipan/gsheet-rest-api/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/api/route.ts: -------------------------------------------------------------------------------- 1 | export async function GET() { 2 | return Response.json({ message: 'Welcome to GSheet Rest API!' }) 3 | } 4 | -------------------------------------------------------------------------------- /src/app/docs/md.module.css: -------------------------------------------------------------------------------- 1 | .md :global(.introduction-description .markdown img) { 2 | display: inline-flex; 3 | margin: 3px 0; 4 | } 5 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | '@tailwindcss/postcss': {}, 5 | }, 6 | } 7 | 8 | export default config 9 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | # https://dev.to/vvo/how-to-add-firebase-service-account-json-files-to-vercel-ph5 2 | # Make sure to add single quote here 3 | GOOGLE_CREDENTIALS='Your JSON value' 4 | 5 | ALLOWED_ORIGINS=http://localhost:3000,https://gsheet-rest-api.vercel.app 6 | API_KEY=YOUR_SECRET_TOKEN 7 | BASE_URL=http://localhost:3000 -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | // prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs 2 | 3 | /** 4 | * @see https://prettier.io/docs/en/configuration.html 5 | * @type {import("prettier").Config} 6 | */ 7 | const config = { 8 | trailingComma: 'es5', 9 | useTabs: false, 10 | tabWidth: 2, 11 | semi: false, 12 | singleQuote: true, 13 | bracketSpacing: true, 14 | plugins: ['prettier-plugin-tailwindcss'], 15 | } 16 | 17 | export default config 18 | -------------------------------------------------------------------------------- /src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Credits to: 3 | * - @shadcn/ui 4 | * 5 | * Based on this doc: 6 | * https://ui.shadcn.com/docs/dark-mode/next 7 | */ 8 | 9 | 'use client' 10 | 11 | import * as React from 'react' 12 | import { ThemeProvider as NextThemesProvider } from 'next-themes' 13 | 14 | export function ThemeProvider({ 15 | children, 16 | ...props 17 | }: React.ComponentProps) { 18 | return {children} 19 | } 20 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path' 2 | import { fileURLToPath } from 'url' 3 | import { FlatCompat } from '@eslint/eslintrc' 4 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' 5 | 6 | const __filename = fileURLToPath(import.meta.url) 7 | const __dirname = dirname(__filename) 8 | 9 | const compat = new FlatCompat({ 10 | baseDirectory: __dirname, 11 | }) 12 | 13 | const eslintConfig = [ 14 | eslintPluginPrettierRecommended, 15 | ...compat.extends('next/core-web-vitals', 'next/typescript'), 16 | ] 17 | 18 | export default eslintConfig 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | api-project-*.json -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/app/docs/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ApiReferenceReact } from '@scalar/api-reference-react' 4 | import styles from './md.module.css' 5 | import clsx from 'clsx' 6 | 7 | // import '@scalar/api-reference-react/style.css' 8 | 9 | export default function References() { 10 | return ( 11 |
12 | 28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @custom-variant dark (&:where(.dark, .dark *)); 4 | 5 | /* 6 | The default border color has changed to `currentColor` in Tailwind CSS v4, 7 | so we've added these compatibility styles to make sure everything still 8 | looks the same as it did with Tailwind CSS v3. 9 | 10 | If we ever want to remove these styles, we need to add an explicit border 11 | color utility to any element that depends on these defaults. 12 | */ 13 | @layer base { 14 | *, 15 | ::after, 16 | ::before, 17 | ::backdrop, 18 | ::file-selector-button { 19 | border-color: var(--color-gray-200, currentColor); 20 | } 21 | } 22 | 23 | body { 24 | font-family: ui-sans-serif, system-ui, Arial, Helvetica, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; 25 | } 26 | 27 | .blur-bg { 28 | background-color: #3166b480; 29 | background: radial-gradient(ellipse closest-side at center, #83dd9e, #3166b480); 30 | } -------------------------------------------------------------------------------- /src/components/theme-switcher.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useTheme } from 'next-themes' 4 | import { MoonIcon, SunIcon } from '@heroicons/react/16/solid' 5 | 6 | export function ThemeSwitcher() { 7 | const { setTheme, theme } = useTheme() 8 | return ( 9 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Irfan Maulana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Credits to: 3 | * - https://github.com/melalj 4 | * 5 | * Most of the code in this file are coming from 6 | * https://github.com/melalj/gsheet-api/blob/master/src/utils.js 7 | * 8 | * Adding small typings and ignore the rest 9 | */ 10 | 11 | const START_ASCII_A = 65 // A -> Z is 65 to 90. Read: https://www.ibm.com/docs/en/sdse/6.4.0?topic=configuration-ascii-characters-from-33-126 12 | 13 | const MAX_LETTER = 26 14 | 15 | export function numberToLetter(num: number) { 16 | let result = '' 17 | let x = 1 18 | let y = MAX_LETTER 19 | let left = num - x 20 | 21 | while (left >= 0) { 22 | const curr = (left % y) / x 23 | result = String.fromCharCode(curr + START_ASCII_A) + result 24 | 25 | x = y 26 | y *= MAX_LETTER 27 | left -= y 28 | } 29 | 30 | return result 31 | } 32 | 33 | export function detectValues(val: string) { 34 | if (!val || val === '') return null 35 | 36 | if (val?.toUpperCase() === 'TRUE') return true 37 | if (val?.toUpperCase() === 'FALSE') return false 38 | if (/^\d+\.\d+$/.test(val)) return parseFloat(val) 39 | if (/^\d+$/.test(val)) return parseInt(val, 10) 40 | 41 | return val 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gsheet-rest-api", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@heroicons/react": "^2.2.0", 13 | "@scalar/api-reference-react": "^0.4.24", 14 | "@scalar/nextjs-api-reference": "^0.5.6", 15 | "ajv": "^8.17.1", 16 | "class-variance-authority": "^0.7.1", 17 | "clsx": "^2.1.1", 18 | "googleapis": "^144.0.0", 19 | "next": "15.1.6", 20 | "next-themes": "^0.4.4", 21 | "react": "^19.0.0", 22 | "react-dom": "^19.0.0" 23 | }, 24 | "devDependencies": { 25 | "@eslint/eslintrc": "^3.2.0", 26 | "@tailwindcss/postcss": "^4.0.3", 27 | "@types/node": "^22.13.1", 28 | "@types/react": "^19.0.8", 29 | "@types/react-dom": "^19.0.3", 30 | "eslint": "^9.19.0", 31 | "eslint-config-next": "15.1.6", 32 | "eslint-config-prettier": "^10.0.1", 33 | "eslint-plugin-prettier": "^5.2.3", 34 | "prettier": "^3.4.2", 35 | "prettier-plugin-tailwindcss": "^0.6.11", 36 | "postcss": "^8.5.1", 37 | "tailwindcss": "^4.0.3", 38 | "typescript": "^5.7.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { Geist, Geist_Mono } from 'next/font/google' 3 | import './globals.css' 4 | import { ThemeProvider } from '@/components/theme-provider' 5 | 6 | const geistSans = Geist({ 7 | variable: '--font-geist-sans', 8 | subsets: ['latin'], 9 | }) 10 | 11 | const geistMono = Geist_Mono({ 12 | variable: '--font-geist-mono', 13 | subsets: ['latin'], 14 | }) 15 | 16 | export const metadata: Metadata = { 17 | title: 'GSheet Rest API', 18 | description: 19 | 'Simple yet deployable rest API for your Google Sheet. Turn your Google Sheet into API.', 20 | } 21 | 22 | export default function RootLayout({ 23 | children, 24 | }: Readonly<{ 25 | children: React.ReactNode 26 | }>) { 27 | return ( 28 | 29 | 32 | 38 | {children} 39 | 40 | 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | 3 | const allowedOrigins = (process.env.ALLOWED_ORIGINS || '') 4 | .split(',') 5 | .map((o) => o.trim()) 6 | 7 | const corsOptions = { 8 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 9 | 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 10 | } 11 | 12 | export function middleware(request: NextRequest) { 13 | // Check the origin from the request 14 | const origin = request.headers.get('origin') ?? '' 15 | const isAllowedOrigin = allowedOrigins.includes(origin) 16 | 17 | // Handle preflighted requests 18 | const isPreflight = request.method === 'OPTIONS' 19 | 20 | if (isPreflight) { 21 | const preflightHeaders = { 22 | ...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }), 23 | ...corsOptions, 24 | } 25 | return NextResponse.json({}, { headers: preflightHeaders }) 26 | } 27 | 28 | // Handle simple requests 29 | const response = NextResponse.next() 30 | 31 | if (isAllowedOrigin) { 32 | response.headers.set('Access-Control-Allow-Origin', origin) 33 | } 34 | 35 | Object.entries(corsOptions).forEach(([key, value]) => { 36 | response.headers.set(key, value) 37 | }) 38 | 39 | return response 40 | } 41 | 42 | export const config = { 43 | matcher: '/api/:path*', 44 | } 45 | -------------------------------------------------------------------------------- /src/app/api/v1/sheets/route.ts: -------------------------------------------------------------------------------- 1 | import { getAllFileList } from '@/utils/sheets' 2 | import { headers } from 'next/headers' 3 | import { NextRequest } from 'next/server' 4 | 5 | export async function GET(request: NextRequest) { 6 | const headersList = await headers() 7 | const apiKey = headersList.get('x-api-key') 8 | 9 | if (!apiKey || apiKey !== process.env.API_KEY) { 10 | console.warn(`'Header x-api-key: "${apiKey}" is not valid!'`) 11 | return Response.json( 12 | { 13 | message: `'Header x-api-key: "${apiKey}" is not valid!'`, 14 | data: [], 15 | next_token: '', 16 | }, 17 | { 18 | status: 401, 19 | } 20 | ) 21 | } 22 | 23 | const searchParams = request.nextUrl.searchParams 24 | const limit = searchParams.get('limit') || '10' 25 | const nextToken = searchParams.get('next_token') || '' 26 | 27 | const res = await getAllFileList({ 28 | limit: limit ? parseInt(limit, 10) : 10, 29 | nextToken, 30 | }) 31 | 32 | if (res) { 33 | return Response.json(res) 34 | } else { 35 | return Response.json( 36 | { 37 | message: 38 | 'Can not retrieve any files. Make sure to give access to the service account.', 39 | data: [], 40 | next_token: '', 41 | }, 42 | { 43 | status: 400, 44 | } 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/api/v1/sheets/[spreadsheet_id]/route.ts: -------------------------------------------------------------------------------- 1 | import { getSheetsBySpreadsheetId } from '@/utils/sheets' 2 | import { headers } from 'next/headers' 3 | 4 | export async function GET( 5 | _request: Request, 6 | { params }: { params: Promise<{ spreadsheet_id: string }> } 7 | ) { 8 | const headersList = await headers() 9 | const apiKey = headersList.get('x-api-key') 10 | 11 | if (!apiKey || apiKey !== process.env.API_KEY) { 12 | console.warn(`'Header x-api-key: "${apiKey}" is not valid!'`) 13 | return Response.json( 14 | { 15 | message: `'Header x-api-key: "${apiKey}" is not valid!'`, 16 | data: [], 17 | }, 18 | { 19 | status: 401, 20 | } 21 | ) 22 | } 23 | 24 | const { spreadsheet_id } = await params 25 | 26 | if (!spreadsheet_id) { 27 | return Response.json( 28 | { 29 | message: 'Parameter "spreadsheet_id" is required!', 30 | data: [], 31 | }, 32 | { 33 | status: 400, 34 | } 35 | ) 36 | } 37 | 38 | const res = await getSheetsBySpreadsheetId(spreadsheet_id) 39 | 40 | if (res) { 41 | return Response.json({ 42 | data: res, 43 | }) 44 | } else { 45 | return Response.json( 46 | { 47 | message: 48 | 'Can not retrieve any sheets. Make sure to give access to the service account and double check the "spreadsheet_id" param.', 49 | data: [], 50 | }, 51 | { 52 | status: 400, 53 | } 54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/spinner.tsx: -------------------------------------------------------------------------------- 1 | import { cva, VariantProps } from 'class-variance-authority' 2 | import clsx from 'clsx' 3 | import React from 'react' 4 | 5 | const spinnerVariants = cva( 6 | 'text-gray-200 animate-spin dark:text-gray-600 fill-blue-600', 7 | { 8 | variants: { 9 | size: { 10 | default: 'h-8 w-8', 11 | xs: 'w-4 h-4', 12 | sm: 'w-6 h-6', 13 | md: 'h-8 w-8', 14 | lg: 'w-10 h-10', 15 | }, 16 | }, 17 | defaultVariants: { 18 | size: 'default', 19 | }, 20 | } 21 | ) 22 | 23 | export interface SpinnerProps 24 | extends React.SVGAttributes, 25 | VariantProps { 26 | className?: string 27 | } 28 | 29 | const Spinner = React.forwardRef( 30 | ({ className = '', size, ...props }, ref) => ( 31 |
32 | 50 | Loading... 51 |
52 | ) 53 | ) 54 | 55 | Spinner.displayName = 'Spinner' 56 | 57 | export { Spinner } 58 | -------------------------------------------------------------------------------- /src/app/example/TodoItem.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Spinner } from '@/components/spinner' 4 | import { markAsDone, removeTodo } from './actions' 5 | import clsx from 'clsx' 6 | import { useActionState } from 'react' 7 | import { CheckIcon, TrashIcon } from '@heroicons/react/16/solid' 8 | 9 | export function TodoItem({ 10 | todo, 11 | }: { 12 | todo: { _row: number; ACTIVITY: string; STATUS: boolean } 13 | }) { 14 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 15 | const [_, formActionMarkAsDone, pendingMarkAsDone] = useActionState( 16 | markAsDone, 17 | false 18 | ) 19 | 20 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 21 | const [__, formActionRemove, pendingRemove] = useActionState( 22 | removeTodo, 23 | false 24 | ) 25 | 26 | return ( 27 |
28 | 29 | 30 |
31 | 32 | {todo.ACTIVITY} 33 | 34 |
35 | {!todo.STATUS ? ( 36 | 50 | ) : ( 51 | 61 | )} 62 |
63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📑 GSheet Rest API 2 | 3 | Effortless REST API for your Google Sheet. Instantly turn your Google Sheet into a powerful API. 4 | 5 | - Home: [gsheet-rest-api.vercel.app](https://gsheet-rest-api.vercel.app/) 6 | - API Doc: [gsheet-rest-api.vercel.app/docs](https://gsheet-rest-api.vercel.app/docs) 7 | - Example App: [gsheet-rest-api.vercel.app/example](https://gsheet-rest-api.vercel.app/example) 8 | 9 | ## Setup access 10 | 11 | + Enable [Google Sheets API](https://console.developers.google.com/apis/api/sheets.googleapis.com/overview) + [Google Drive API](https://console.developers.google.com/apis/api/drive.googleapis.com/overview) in your Cloud Console 12 | + Create new [service account](https://console.cloud.google.com/iam-admin/serviceaccounts) 13 | + Add new keys in your service account 14 | - Download the JSON file, and put it in the `.env.local`. [Read this article](https://dev.to/vvo/how-to-add-firebase-service-account-json-files-to-vercel-ph5) 15 | + Grant your service account access to the speadsheet 16 | - Share the spreadsheet 17 | - Add people with email from your service account 18 | - Click "Copy link" button to get the `spreadsheetid` (e.g: If the link is `https://docs.google.com/spreadsheets/d/1-Qi5_aizQiNTMRBuqboory9Ba7lyonxCjDCogASdVdg/edit?usp=sharing` then the ID is `1-Qi5_aizQiNTMRBuqboory9Ba7lyonxCjDCogASdVdg`) 19 | 20 | ## Development 21 | 22 | - Install dependencies 23 | 24 | ```bash 25 | bun install 26 | ``` 27 | 28 | - Run project 29 | 30 | ```bash 31 | bun run dev 32 | ``` 33 | 34 | ## Deploy on Vercel 35 | 36 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fmazipan%2Fgsheet-rest-api) 37 | 38 | Add environment variable `GOOGLE_CREDENTIALS` with your JSON from service account key. 39 | 40 | ## Limitations and Quota 41 | 42 | [Following Google Sheets API documentation](https://developers.google.com/sheets/api/limits). This version of the Google Sheets API has a limit of 500 requests per 100 seconds per project, and 100 requests per 100 seconds per user. Limits for reads and writes are tracked separately. There is no daily usage limit. 43 | 44 | Be mindful about this limitation, if you want to use this api as a backend for your frontend! 45 | 46 | ## Credits 47 | 48 | - [melalj/gsheet-api](https://github.com/melalj/gsheet-api) 49 | - [openais-io/sheepdb](https://github.com/openais-io/sheepdb) 50 | 51 | --- 52 | 53 | ⓒ since 2025, By Irfan Maulana -------------------------------------------------------------------------------- /src/app/example/actions.tsx: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { revalidatePath, revalidateTag } from 'next/cache' 4 | import { redirect } from 'next/navigation' 5 | 6 | const SPREADSHEET_ID = '1OPctiEOSqDXEW040kGEVzDc8crA6Da7Gb36ukAdNjkE' 7 | const SHEET_NAME = 'Sheet1' 8 | 9 | export async function markAsDone( 10 | _previousState: boolean, 11 | formData: FormData 12 | ): Promise { 13 | const apiUrl = `${process.env.BASE_URL}/api/v1/sheets/${SPREADSHEET_ID}/${SHEET_NAME}` 14 | 15 | const row = formData.get('row') 16 | 17 | const res = await fetch(apiUrl, { 18 | method: 'PUT', 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | 'x-api-key': process.env.API_KEY || '', 22 | }, 23 | body: JSON.stringify({ 24 | [row as string]: { 25 | STATUS: 'TRUE', 26 | }, 27 | }), 28 | }) 29 | 30 | await res.json() 31 | 32 | revalidatePath('/example') 33 | revalidateTag('todos') 34 | return true 35 | } 36 | 37 | export async function addNewTodo( 38 | _previousState: boolean, 39 | formData: FormData 40 | ): Promise { 41 | const apiUrl = `${process.env.BASE_URL}/api/v1/sheets/${SPREADSHEET_ID}/${SHEET_NAME}` 42 | 43 | const activity = formData.get('activity') 44 | 45 | const res = await fetch(apiUrl, { 46 | method: 'POST', 47 | headers: { 48 | 'Content-Type': 'application/json', 49 | 'x-api-key': process.env.API_KEY || '', 50 | }, 51 | body: JSON.stringify({ 52 | data: [ 53 | { 54 | ACTIVITY: activity, 55 | STATUS: 'FALSE', 56 | }, 57 | ], 58 | }), 59 | }) 60 | 61 | await res.json() 62 | 63 | revalidatePath('/example') 64 | revalidateTag('todos') 65 | redirect('/example') 66 | } 67 | 68 | export async function removeTodo( 69 | _previousState: boolean, 70 | formData: FormData 71 | ): Promise { 72 | const row = formData.get('row') 73 | 74 | const apiUrl = `${process.env.BASE_URL}/api/v1/sheets/${SPREADSHEET_ID}/${SHEET_NAME}/${row}` 75 | 76 | const res = await fetch(apiUrl, { 77 | method: 'DELETE', 78 | headers: { 79 | 'Content-Type': 'application/json', 80 | 'x-api-key': process.env.API_KEY || '', 81 | }, 82 | }) 83 | 84 | await res.json() 85 | 86 | revalidatePath('/example') 87 | revalidateTag('todos') 88 | 89 | return true 90 | } 91 | 92 | function delay(time: number) { 93 | return new Promise((resolve) => setTimeout(resolve, time)) 94 | } 95 | 96 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 97 | export async function refreshCache(__previousState: boolean): Promise { 98 | await delay(500) 99 | revalidatePath('/example') 100 | revalidateTag('todos') 101 | 102 | return true 103 | } 104 | -------------------------------------------------------------------------------- /src/app/api/v1/sheets/[spreadsheet_id]/[sheet_name]/[rows]/route.ts: -------------------------------------------------------------------------------- 1 | import { getSheetDataByRows, removeSheetRows } from '@/utils/sheets' 2 | import { headers } from 'next/headers' 3 | import { NextRequest } from 'next/server' 4 | 5 | export async function GET( 6 | request: NextRequest, 7 | { 8 | params, 9 | }: { 10 | params: Promise<{ 11 | spreadsheet_id: string 12 | sheet_name: string 13 | rows: string 14 | }> 15 | } 16 | ) { 17 | const headersList = await headers() 18 | const apiKey = headersList.get('x-api-key') 19 | 20 | if (!apiKey || apiKey !== process.env.API_KEY) { 21 | console.warn(`'Header x-api-key: "${apiKey}" is not valid!'`) 22 | return Response.json( 23 | { 24 | message: `'Header x-api-key: "${apiKey}" is not valid!'`, 25 | data: null, 26 | }, 27 | { 28 | status: 401, 29 | } 30 | ) 31 | } 32 | 33 | const searchParams = request.nextUrl.searchParams 34 | const columnCount = searchParams.get('column_count') 35 | 36 | const { spreadsheet_id, sheet_name, rows } = await params 37 | 38 | if (!spreadsheet_id || !sheet_name || !rows) { 39 | return Response.json( 40 | { 41 | message: 42 | 'Parameter "spreadsheet_id", "sheet_name" and "row" are required!', 43 | data: null, 44 | }, 45 | { 46 | status: 400, 47 | } 48 | ) 49 | } 50 | 51 | const res = await getSheetDataByRows(spreadsheet_id, sheet_name, rows, { 52 | columnCount: columnCount ? parseInt(columnCount, 10) : 10, 53 | }) 54 | 55 | if (res) { 56 | return Response.json(res) 57 | } 58 | 59 | return Response.json( 60 | { 61 | message: 62 | 'Can not retrieve any data in this sheet name. Make sure to give access to the service account and double check the "spreadsheet_id", "sheet_name" and "row" param.', 63 | data: null, 64 | }, 65 | { 66 | status: 400, 67 | } 68 | ) 69 | } 70 | 71 | export async function DELETE( 72 | _request: NextRequest, 73 | { 74 | params, 75 | }: { 76 | params: Promise<{ 77 | spreadsheet_id: string 78 | sheet_name: string 79 | rows: string 80 | }> 81 | } 82 | ) { 83 | const headersList = await headers() 84 | const apiKey = headersList.get('x-api-key') 85 | 86 | if (!apiKey || apiKey !== process.env.API_KEY) { 87 | console.warn(`'Header x-api-key: "${apiKey}" is not valid!'`) 88 | return Response.json( 89 | { 90 | message: `'Header x-api-key: "${apiKey}" is not valid!'`, 91 | data: [], 92 | }, 93 | { 94 | status: 401, 95 | } 96 | ) 97 | } 98 | 99 | const { spreadsheet_id, sheet_name, rows } = await params 100 | const deletedRows = (rows || '').split(',') 101 | 102 | if (!spreadsheet_id || !sheet_name) { 103 | return Response.json( 104 | { 105 | message: 'Parameter "spreadsheet_id" and "sheet_name" are required!', 106 | deleted_rows: 0, 107 | }, 108 | { 109 | status: 400, 110 | } 111 | ) 112 | } 113 | 114 | if (deletedRows.length === 0) { 115 | return Response.json( 116 | { 117 | message: 'Require body parameter "rows"!', 118 | deleted_rows: 0, 119 | }, 120 | { 121 | status: 400, 122 | } 123 | ) 124 | } 125 | 126 | const res = await removeSheetRows(spreadsheet_id, sheet_name, deletedRows) 127 | 128 | if (res) { 129 | return Response.json(res) 130 | } 131 | 132 | return Response.json( 133 | { 134 | message: 'Failed delete rows.', 135 | deleted_rows: 0, 136 | }, 137 | { 138 | status: 400, 139 | } 140 | ) 141 | } 142 | -------------------------------------------------------------------------------- /src/app/example/AddNewTodo.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Spinner } from '@/components/spinner' 4 | import { addNewTodo, refreshCache } from './actions' 5 | import { useActionState, useState } from 'react' 6 | import { BoltIcon, PlusIcon } from '@heroicons/react/16/solid' 7 | 8 | export function AddNewTodo() { 9 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 10 | const [_, formAction, pendingSubmission] = useActionState(addNewTodo, false) 11 | 12 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 13 | const [__, formActionRefreshCache, pendingRefresh] = useActionState( 14 | refreshCache, 15 | false 16 | ) 17 | 18 | const [showForm, setShowForm] = useState(false) 19 | 20 | return ( 21 |
22 |
23 | 33 | 47 |
48 | {showForm && ( 49 |
50 |
51 | 57 | 65 |
66 |
67 | 77 | 88 |
89 |
90 | )} 91 |
92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Link from 'next/link' 4 | import { CursorArrowRaysIcon } from '@heroicons/react/16/solid' 5 | import { ThemeSwitcher } from '@/components/theme-switcher' 6 | import { BookOpenIcon, CubeIcon } from '@heroicons/react/24/outline' 7 | 8 | export default function Home() { 9 | return ( 10 |
11 |
12 |
13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |

24 | 📑 GSheet Rest API 25 |

26 |

27 | Effortless REST API for your Google Sheet. 28 |
29 | Instantly turn your Google Sheet into a powerful API. 30 |

31 |
32 | 33 |
34 | 40 | 46 | 50 | 51 | Deploy now 52 | 53 | 58 | 59 | Docs 60 | 61 | 66 | 67 | Example 68 | 69 |
70 |
71 | 83 |
84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /src/app/api/v1/sheets/[spreadsheet_id]/[sheet_name]/route.ts: -------------------------------------------------------------------------------- 1 | import { 2 | appendSheetRow, 3 | getDataBySheetName, 4 | updateSheetRow, 5 | } from '@/utils/sheets' 6 | import { headers } from 'next/headers' 7 | import { NextRequest } from 'next/server' 8 | 9 | export async function GET( 10 | request: NextRequest, 11 | { 12 | params, 13 | }: { params: Promise<{ spreadsheet_id: string; sheet_name: string }> } 14 | ) { 15 | const headersList = await headers() 16 | const apiKey = headersList.get('x-api-key') 17 | 18 | if (!apiKey || apiKey !== process.env.API_KEY) { 19 | console.warn(`'Header x-api-key: "${apiKey}" is not valid!'`) 20 | return Response.json( 21 | { 22 | message: `'Header x-api-key: "${apiKey}" is not valid!'`, 23 | data: [], 24 | }, 25 | { 26 | status: 401, 27 | } 28 | ) 29 | } 30 | 31 | const searchParams = request.nextUrl.searchParams 32 | const offset = searchParams.get('offset') 33 | const limit = searchParams.get('limit') 34 | const columnCount = searchParams.get('column_count') 35 | 36 | const { spreadsheet_id, sheet_name } = await params 37 | 38 | if (!spreadsheet_id || !sheet_name) { 39 | return Response.json( 40 | { 41 | message: 'Parameter "spreadsheet_id" and "sheet_name" are required!', 42 | columns: null, 43 | pagination: null, 44 | data: [], 45 | }, 46 | { 47 | status: 400, 48 | } 49 | ) 50 | } 51 | 52 | const res = await getDataBySheetName(spreadsheet_id, sheet_name, { 53 | offset: offset ? parseInt(offset, 10) : 2, 54 | limit: limit ? parseInt(limit, 10) : 100, 55 | columnCount: columnCount ? parseInt(columnCount, 10) : 10, 56 | }) 57 | 58 | if (res) { 59 | return Response.json(res) 60 | } 61 | 62 | return Response.json( 63 | { 64 | message: 65 | 'Can not retrieve any data in this sheet name. Make sure to give access to the service account and double check the "spreadsheet_id" and "sheet_name" param.', 66 | columns: null, 67 | pagination: null, 68 | data: [], 69 | }, 70 | { 71 | status: 400, 72 | } 73 | ) 74 | } 75 | 76 | // --- Update rows 77 | export async function PUT( 78 | request: NextRequest, 79 | { 80 | params, 81 | }: { params: Promise<{ spreadsheet_id: string; sheet_name: string }> } 82 | ) { 83 | const headersList = await headers() 84 | const apiKey = headersList.get('x-api-key') 85 | 86 | if (!apiKey || apiKey !== process.env.API_KEY) { 87 | console.warn(`'Header x-api-key: "${apiKey}" is not valid!'`) 88 | return Response.json( 89 | { 90 | message: `'Header x-api-key: "${apiKey}" is not valid!'`, 91 | data: [], 92 | }, 93 | { 94 | status: 401, 95 | } 96 | ) 97 | } 98 | 99 | const searchParams = request.nextUrl.searchParams 100 | 101 | const inputOptions = searchParams.get('input_options') || 'USER_ENTERED' 102 | const columnCount = searchParams.get('column_count') 103 | 104 | const { spreadsheet_id, sheet_name } = await params 105 | 106 | if (!spreadsheet_id || !sheet_name) { 107 | return Response.json( 108 | { 109 | message: 'Parameter "spreadsheet_id" and "sheet_name" are required!', 110 | data: [], 111 | }, 112 | { 113 | status: 400, 114 | } 115 | ) 116 | } 117 | 118 | const body = await request.json() 119 | 120 | if (!body) { 121 | return Response.json( 122 | { 123 | message: 'Body is required to invoke update row function!', 124 | data: [], 125 | }, 126 | { 127 | status: 400, 128 | } 129 | ) 130 | } 131 | 132 | const res = await updateSheetRow(spreadsheet_id, sheet_name, body, { 133 | columnCount: columnCount ? parseInt(columnCount, 10) : 10, 134 | valueInputOption: inputOptions as 'USER_ENTERED' | 'RAW', 135 | }) 136 | 137 | if (res) { 138 | return Response.json({ 139 | data: res, 140 | }) 141 | } 142 | 143 | return Response.json( 144 | { 145 | message: 'Failed when update the data', 146 | data: [], 147 | }, 148 | { 149 | status: 400, 150 | } 151 | ) 152 | } 153 | 154 | // --- Append rows 155 | export async function POST( 156 | request: NextRequest, 157 | { 158 | params, 159 | }: { params: Promise<{ spreadsheet_id: string; sheet_name: string }> } 160 | ) { 161 | const headersList = await headers() 162 | const apiKey = headersList.get('x-api-key') 163 | 164 | if (!apiKey || apiKey !== process.env.API_KEY) { 165 | console.warn(`'Header x-api-key: "${apiKey}" is not valid!'`) 166 | return Response.json( 167 | { 168 | message: `'Header x-api-key: "${apiKey}" is not valid!'`, 169 | data: [], 170 | }, 171 | { 172 | status: 401, 173 | } 174 | ) 175 | } 176 | 177 | const searchParams = request.nextUrl.searchParams 178 | 179 | const inputOptions = searchParams.get('input_options') || 'USER_ENTERED' 180 | const columnCount = searchParams.get('column_count') 181 | 182 | const { spreadsheet_id, sheet_name } = await params 183 | 184 | if (!spreadsheet_id || !sheet_name) { 185 | return Response.json( 186 | { 187 | message: 'Parameter "spreadsheet_id" and "sheet_name" are required!', 188 | data: [], 189 | }, 190 | { 191 | status: 400, 192 | } 193 | ) 194 | } 195 | 196 | const body = await request.json() 197 | 198 | if (!body || !body.data || body.data.length === 0) { 199 | return Response.json( 200 | { 201 | message: 'Body "data" is required to invoke append row function!', 202 | data: [], 203 | }, 204 | { 205 | status: 400, 206 | } 207 | ) 208 | } 209 | 210 | const res = await appendSheetRow(spreadsheet_id, sheet_name, body.data, { 211 | columnCount: columnCount ? parseInt(columnCount, 10) : 10, 212 | valueInputOption: inputOptions as 'USER_ENTERED' | 'RAW', 213 | }) 214 | 215 | if (res) { 216 | return Response.json({ 217 | data: res, 218 | }) 219 | } 220 | 221 | return Response.json( 222 | { 223 | message: 'Failed when update the data', 224 | data: [], 225 | }, 226 | { 227 | status: 400, 228 | } 229 | ) 230 | } 231 | -------------------------------------------------------------------------------- /src/app/example/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { TodoItem } from './TodoItem' 3 | import { AddNewTodo } from './AddNewTodo' 4 | import { ArrowLeftIcon, ListBulletIcon } from '@heroicons/react/16/solid' 5 | import { BookOpenIcon, CubeIcon } from '@heroicons/react/24/outline' 6 | import { ThemeSwitcher } from '@/components/theme-switcher' 7 | import { notFound } from 'next/navigation' 8 | 9 | const SPREADSHEET_ID = '1OPctiEOSqDXEW040kGEVzDc8crA6Da7Gb36ukAdNjkE' 10 | const SHEET_NAME = 'Sheet1' 11 | 12 | type Todo = { _row: number; ACTIVITY: string; STATUS: boolean } 13 | 14 | type TodosResponse = { 15 | columns: { title: string; cell: string }[] 16 | data: Todo[] 17 | pagination: { 18 | limit: number 19 | cell_range: string 20 | offset: number 21 | next_offset: number 22 | total: number 23 | hasNext: boolean 24 | } 25 | } 26 | 27 | export const dynamic = 'force-dynamic' 28 | 29 | export default async function Example() { 30 | const apiUrl = `${process.env.BASE_URL}/api/v1/sheets/${SPREADSHEET_ID}/${SHEET_NAME}` 31 | 32 | const res = await fetch(apiUrl, { 33 | cache: 'force-cache', 34 | next: { 35 | tags: ['todos'], 36 | revalidate: 3600, 37 | }, 38 | headers: { 39 | 'x-api-key': process.env.API_KEY || '', 40 | }, 41 | }) 42 | 43 | if (!res.ok) { 44 | notFound() 45 | } 46 | 47 | const todosResponse: TodosResponse = await res.json() 48 | 49 | return ( 50 |
51 | 69 | 70 |
71 |

72 | 73 | Todo List App 74 |

75 | 76 |

77 | This app was developed to demonstrate how to use{' '} 78 | 79 | gsheet-rest-api 80 | {' '} 81 | to build your next hobby project. 82 |
83 | 84 | You can find the complete code for this example in{' '} 85 | 91 | src/app/example 92 | 93 | . 94 | 95 |

96 | 97 | 98 | 99 |
    100 | {todosResponse.data.map((todo: Todo) => { 101 | return ( 102 |
  • 103 | 104 |
  • 105 | ) 106 | })} 107 |
108 | 109 |
110 | 111 | Total: {todosResponse?.pagination?.total} 112 | 113 | 114 | Next:{' '} 115 | {todosResponse?.pagination?.hasNext?.toString()?.toUpperCase()} 116 | 117 | 118 | Cell Range: {todosResponse?.pagination?.cell_range} 119 | 120 | 121 | Limit: {todosResponse?.pagination?.limit} 122 | 123 | 124 | Offset: {todosResponse?.pagination?.offset} 125 | 126 | 127 | Next Offset: {todosResponse?.pagination?.next_offset} 128 | 129 |
130 |
131 | 132 | {/*
133 |

Google Sheet iFrame

134 |

135 | This is the Google Sheet iframe that shows the exact sheet we used. 136 | You can use it to compare the data with the list above. 137 |

138 | 142 |
*/} 143 | 144 | 156 |
157 | ) 158 | } 159 | -------------------------------------------------------------------------------- /src/utils/sheets.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Credits to: 3 | * - https://github.com/melalj 4 | * 5 | * Most of the code in this file are coming from 6 | * https://github.com/melalj/gsheet-api/blob/master/src/api/gsheet.js 7 | * 8 | * Refine the typing to have better intellisense 9 | * Fix some deprecated parameter when passing to newer "sheets_v4" api 10 | */ 11 | 12 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 13 | import { google, type sheets_v4 } from 'googleapis' 14 | import { detectValues, numberToLetter } from './utils' 15 | 16 | // :oad the environment variable with our keys 17 | const keysEnvVar = process.env.GOOGLE_CREDENTIALS as string 18 | if (!keysEnvVar) { 19 | throw new Error('The $GOOGLE_CREDENTIALS environment variable was not found!') 20 | } 21 | 22 | const env = JSON.parse(keysEnvVar) 23 | 24 | const auth = new google.auth.JWT({ 25 | email: env.client_email, 26 | key: env.private_key, 27 | scopes: ['https://www.googleapis.com/auth/drive'], 28 | }) 29 | 30 | const drive = google.drive({ version: 'v3', auth }) 31 | const sheets = google.sheets({ version: 'v4', auth }) 32 | 33 | export type FileData = { 34 | id: string 35 | name: string 36 | /** 37 | * @sample: In format "2025-02-03T08:58:19.749Z" 38 | */ 39 | modifiedTime: string 40 | } 41 | 42 | export type GetSpreadsheetOptions = { 43 | limit?: number 44 | nextToken?: string 45 | } 46 | 47 | export async function getAllFileList( 48 | options: GetSpreadsheetOptions = { limit: 10, nextToken: '' } 49 | ): Promise<{ 50 | next_token: string 51 | data: FileData[] | null 52 | }> { 53 | try { 54 | const driveRes = await drive.files.list({ 55 | q: "trashed = false and mimeType = 'application/vnd.google-apps.spreadsheet'", 56 | fields: 'files(id, name, modifiedTime)', 57 | spaces: 'drive', 58 | pageSize: options.limit, 59 | pageToken: options.nextToken ? undefined : options.nextToken, 60 | }) 61 | 62 | return { 63 | next_token: driveRes.data.nextPageToken || '', 64 | data: (driveRes.data.files || []) as FileData[], 65 | } 66 | } catch (e) { 67 | console.error('Got error when invoke getAllFileList', e) 68 | return { 69 | next_token: '', 70 | data: null, 71 | } 72 | } 73 | } 74 | 75 | export type GetSheetsBySpreadsheetIdResponse = { 76 | title: string 77 | index: number 78 | sheetId: number 79 | rowCount: number 80 | columnCount: number 81 | } 82 | 83 | export async function getSheetsBySpreadsheetId( 84 | spreadsheetId: string 85 | ): Promise { 86 | try { 87 | const sheetRes = await sheets.spreadsheets.get({ 88 | spreadsheetId, 89 | }) 90 | 91 | if (sheetRes && sheetRes.data && sheetRes.data.sheets) { 92 | const output = sheetRes.data.sheets.map( 93 | (d) => 94 | ({ 95 | title: d?.properties?.title, 96 | index: d?.properties?.index, 97 | sheetId: d?.properties?.sheetId, 98 | rowCount: d?.properties?.gridProperties?.rowCount, 99 | columnCount: d?.properties?.gridProperties?.columnCount, 100 | }) as GetSheetsBySpreadsheetIdResponse 101 | ) 102 | 103 | return output 104 | } else { 105 | console.warn('Got empty result when invoke getSheetsBySpreadsheetId') 106 | return null 107 | } 108 | } catch (e) { 109 | console.error('Got error when invoke getSheetsBySpreadsheetId', e) 110 | return null 111 | } 112 | } 113 | 114 | export type DataBySheetIdOptions = { 115 | offset?: number 116 | limit?: number 117 | columnCount?: number 118 | } 119 | 120 | export type SheetColumn = { 121 | title: string 122 | cell: string 123 | } 124 | 125 | export type SheetPagination = { 126 | limit: number 127 | cell_range: string 128 | offset: number 129 | next_offset: number 130 | total: number 131 | hasNext: boolean 132 | } 133 | 134 | export type GetDataBySheetNameResponse = { 135 | columns: SheetColumn[] 136 | pagination: SheetPagination 137 | data: Record[] 138 | } 139 | 140 | export async function getDataBySheetName( 141 | spreadsheetId: string, 142 | sheetName: string, 143 | options: DataBySheetIdOptions = { offset: 2, limit: 100, columnCount: 10 } 144 | ): Promise { 145 | try { 146 | const offset = options?.offset || 2 147 | const limit = options?.limit || 100 148 | const columnCount = options?.columnCount || 10 149 | 150 | const firstRow = offset 151 | const lastRow = offset + limit - 1 152 | 153 | const maxColumn = columnCount ? numberToLetter(Number(columnCount)) : 'EE' 154 | 155 | const headerRange = `${sheetName}!A1:${maxColumn}1` 156 | const countRange = `${sheetName}!A2:A` 157 | const paginatedRange = `${sheetName}!A${firstRow}:${maxColumn}${lastRow}` 158 | 159 | const sheetRes = await sheets.spreadsheets.values.batchGet({ 160 | spreadsheetId, 161 | ranges: [headerRange, countRange, paginatedRange], 162 | }) 163 | 164 | if (sheetRes && sheetRes.data && sheetRes.data.valueRanges) { 165 | const headerRow = sheetRes.data.valueRanges?.[0]?.values?.[0] || [] 166 | const total = (sheetRes.data.valueRanges[1].values || []).length 167 | const rows = sheetRes.data.valueRanges[2].values || [] 168 | 169 | const columns: SheetColumn[] = [] 170 | 171 | headerRow.forEach((columnName, columnIndex) => { 172 | columns.push({ 173 | title: columnName, 174 | cell: `${sheetName}!${numberToLetter(columnIndex + 1)}`, 175 | }) 176 | }) 177 | 178 | const data = [] 179 | for (let i = 0; i < rows.length; i += 1) { 180 | const row = {} 181 | let validValuesCount = 0 182 | // @ts-ignore 183 | row._row = firstRow + i 184 | headerRow.forEach((columnName, columnIndex) => { 185 | // @ts-ignore 186 | row[columnName] = detectValues(rows[i][columnIndex]) 187 | 188 | // @ts-ignore 189 | if (row[columnName]) validValuesCount += 1 190 | }) 191 | 192 | if (validValuesCount) data.push(row) 193 | } 194 | 195 | const hasNext = total > offset + limit 196 | 197 | const pagination: SheetPagination = { 198 | limit, 199 | cell_range: paginatedRange, 200 | offset, 201 | next_offset: !hasNext ? offset : offset + limit, 202 | total, 203 | hasNext, 204 | } 205 | 206 | return { columns, pagination, data } as GetDataBySheetNameResponse 207 | } else { 208 | console.warn('Got empty result when invoke getDataBySheetId') 209 | return null 210 | } 211 | } catch (e) { 212 | console.error('Got error when invoke getDataBySheetName', e) 213 | return null 214 | } 215 | } 216 | 217 | export async function removeSheetRows( 218 | spreadsheetId: string, 219 | sheetName: string, 220 | rows: string[] 221 | ) { 222 | try { 223 | // Get sheetId 224 | const sheetRes = await sheets.spreadsheets.get({ 225 | spreadsheetId, 226 | }) 227 | 228 | if (!sheetRes || !sheetRes.data || !sheetRes.data.sheets) return null 229 | 230 | const sheetInfo = sheetRes.data.sheets.find( 231 | (d) => d?.properties?.title === sheetName 232 | ) 233 | 234 | if (!sheetInfo) return null 235 | 236 | const sheetId = sheetInfo.properties?.sheetId 237 | 238 | // Build requests (we should delete from the bottom to top) 239 | const rowNumbers = rows.map((d) => Number(d)) 240 | rowNumbers.sort((a, b) => b - a) 241 | 242 | const requests: sheets_v4.Schema$Request[] = [] 243 | rowNumbers.forEach((endIndex: number) => { 244 | const deleteDimension = { 245 | range: { 246 | sheetId, 247 | dimension: 'ROWS', 248 | startIndex: endIndex - 1, 249 | endIndex, 250 | }, 251 | } as sheets_v4.Schema$DeleteDimensionRequest 252 | 253 | requests.push({ 254 | deleteDimension, 255 | } as sheets_v4.Schema$Request) 256 | }) 257 | 258 | // Batch Delete 259 | const updatedSheet = await sheets.spreadsheets.batchUpdate({ 260 | spreadsheetId, 261 | requestBody: { 262 | requests, 263 | }, 264 | }) 265 | 266 | return { deleted_rows: updatedSheet?.data?.replies?.length || 0 } 267 | } catch (e) { 268 | console.error('Got error when invoke removeSheetRows', e) 269 | return { deleted_rows: 0 } 270 | } 271 | } 272 | 273 | export type UpdateSheetOptions = { 274 | columnCount?: number 275 | valueInputOption?: 'USER_ENTERED' | 'RAW' 276 | } 277 | 278 | // Sample Body: { "4" : { "email": "john@appleseed.com" }, "1": { "phone": "415-500-7000" } } 279 | export async function updateSheetRow( 280 | spreadsheetId: string, 281 | sheetName: string, 282 | bodyData: Record>, 283 | options: UpdateSheetOptions = { 284 | valueInputOption: 'USER_ENTERED', 285 | columnCount: 10, 286 | } 287 | ): Promise { 288 | try { 289 | const columnCount = options?.columnCount || 10 290 | const maxColumn = columnCount ? numberToLetter(Number(columnCount)) : 'EE' 291 | 292 | const headerRange = `${sheetName}!A1:${maxColumn}1` 293 | const sheetRes = await sheets.spreadsheets.values.get({ 294 | spreadsheetId, 295 | range: headerRange, 296 | }) 297 | 298 | if ( 299 | sheetRes && 300 | sheetRes.data && 301 | sheetRes.data.values && 302 | sheetRes.data.values.length > 0 303 | ) { 304 | const headerRow = sheetRes.data.values[0] 305 | const data: sheets_v4.Schema$ValueRange[] = [] 306 | 307 | // Existing columns 308 | const columns: Record = {} 309 | headerRow.forEach((columnName, columnIndex) => { 310 | columns[columnName] = `${sheetName}!${numberToLetter(columnIndex + 1)}` 311 | }) 312 | 313 | // Build data to update 314 | Object.keys(bodyData).forEach((rowNumber) => { 315 | const body = bodyData[rowNumber] 316 | 317 | // New columns 318 | let newColCount = 0 319 | Object.keys(body).forEach((k) => { 320 | if (!columns[k]) { 321 | newColCount += 1 322 | columns[k] = `${sheetName}!${numberToLetter( 323 | headerRow.length + newColCount 324 | )}` 325 | data.push({ 326 | range: `${columns[k]}1`, 327 | values: [[k]], 328 | }) 329 | } 330 | }) 331 | 332 | // ValueRange 333 | Object.keys(body).forEach((columnName) => { 334 | data.push({ 335 | range: `${columns[columnName]}${rowNumber}`, 336 | values: [[body[columnName]]], 337 | }) 338 | }) 339 | }) 340 | 341 | // Batch Update 342 | const updatedSheet = await sheets.spreadsheets.values.batchUpdate({ 343 | spreadsheetId, 344 | requestBody: { 345 | valueInputOption: options.valueInputOption || 'USER_ENTERED', 346 | data, 347 | }, 348 | }) 349 | 350 | return updatedSheet.data 351 | .responses as sheets_v4.Schema$UpdateValuesResponse 352 | } 353 | 354 | return null 355 | } catch (e) { 356 | console.error('Got error when invoke updateSheetRow', e) 357 | return null 358 | } 359 | } 360 | 361 | // Sample Body: [{ "name": "Jean", "email": "jean@appleseed.com" }, { "name": "Bunny", "email": "bunny@appleseed.com" }] 362 | export async function appendSheetRow( 363 | spreadsheetId: string, 364 | sheetName: string, 365 | bodyData: Record[], 366 | options: UpdateSheetOptions = { 367 | valueInputOption: 'USER_ENTERED', 368 | columnCount: 10, 369 | } 370 | ): Promise { 371 | try { 372 | const columnCount = options?.columnCount || 10 373 | const maxColumn = columnCount ? numberToLetter(Number(columnCount)) : 'EE' 374 | 375 | const defaultRange = `${sheetName}!A:${maxColumn}` 376 | const sheetRes = await sheets.spreadsheets.values.get({ 377 | spreadsheetId, 378 | range: defaultRange, 379 | }) 380 | 381 | if ( 382 | sheetRes && 383 | sheetRes.data && 384 | sheetRes.data.values && 385 | sheetRes.data.values.length > 0 386 | ) { 387 | const rows = sheetRes.data.values 388 | 389 | // Existing columns 390 | const columns: Record = {} 391 | const columnList = (rows ? rows[0] : []).slice() 392 | columnList.forEach((columnName, columnIndex) => { 393 | columns[columnName] = `${sheetName}!${numberToLetter(columnIndex + 1)}` 394 | }) 395 | 396 | // Check if there's new columns 397 | let newColCount = 0 398 | 399 | const data: sheets_v4.Schema$ValueRange[] = [] 400 | bodyData.forEach((body) => { 401 | Object.keys(body).forEach((k) => { 402 | if (!columns[k]) { 403 | newColCount += 1 404 | columns[k] = `${sheetName}!${numberToLetter(columnList.length + 1)}` 405 | columnList.push(k) 406 | data.push({ 407 | range: `${columns[k]}1`, 408 | values: [[k]], 409 | }) 410 | } 411 | }) 412 | }) 413 | 414 | if (newColCount) { 415 | await sheets.spreadsheets.values.batchUpdate({ 416 | spreadsheetId, 417 | requestBody: { 418 | valueInputOption: options.valueInputOption || 'USER_ENTERED', 419 | data, 420 | }, 421 | }) 422 | } 423 | 424 | const values = bodyData.map((r) => columnList.map((c) => r[c] || '')) 425 | 426 | // Append 427 | const appendedSheet = await sheets.spreadsheets.values.append({ 428 | spreadsheetId, 429 | range: defaultRange, 430 | valueInputOption: options.valueInputOption || 'USER_ENTERED', 431 | includeValuesInResponse: true, 432 | insertDataOption: 'INSERT_ROWS', 433 | requestBody: { 434 | values, 435 | }, 436 | }) 437 | 438 | return appendedSheet?.data?.updates || null 439 | } 440 | 441 | return null 442 | } catch (e) { 443 | console.error('Got error when invoke appendSheetRow', e) 444 | return null 445 | } 446 | } 447 | 448 | export async function getSheetDataByRows( 449 | spreadsheetId: string, 450 | sheetName: string, 451 | rowNumber: string, 452 | options = { columnCount: 10 } 453 | ) { 454 | try { 455 | const columnCount = options?.columnCount || 10 456 | const maxColumn = columnCount ? numberToLetter(Number(columnCount)) : 'EE' 457 | 458 | const headerRange = `${sheetName}!A1:${maxColumn}1` 459 | const dataRange = `${sheetName}!A${rowNumber}:${maxColumn}${rowNumber}` 460 | 461 | const sheetRes = await sheets.spreadsheets.values.batchGet({ 462 | spreadsheetId, 463 | ranges: [headerRange, dataRange], 464 | }) 465 | 466 | if (sheetRes && sheetRes.data && sheetRes.data.valueRanges) { 467 | const headerRow = sheetRes.data.valueRanges?.[0]?.values?.[0] || [] 468 | 469 | const rows = sheetRes.data.valueRanges[1].values 470 | 471 | const row: Record = { 472 | _row: Number(rowNumber), 473 | } 474 | 475 | headerRow.forEach((columnName: string, columnIndex) => { 476 | const val = detectValues(rows?.[0]?.[columnIndex]) 477 | if (val) { 478 | row[columnName] = val 479 | } 480 | }) 481 | 482 | return { data: row } 483 | } 484 | 485 | return null 486 | } catch (e) { 487 | console.error('Got error when invoke getSheetDataByRows', e) 488 | return null 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /public/openapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.4", 3 | "info": { 4 | "title": "GSheet Rest API", 5 | "description": "[Home](/) • [Example](/example) • [Open API](/openapi.json) • [Postman](/gsheet-rest-api.postman_collection.json) \n\nEffortless REST API for your Google Sheet. Instantly turn your Google Sheet into a powerful API. \n\n [![GitHub Repo stars](https://img.shields.io/github/stars/mazipan/gsheet-rest-api)](https://github.com/mazipan/gsheet-rest-api) ![GitHub forks](https://img.shields.io/github/forks/mazipan/gsheet-rest-api) \n\n [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fmazipan%2Fgsheet-rest-api)", 6 | "version": "1.0.0" 7 | }, 8 | "servers": [ 9 | { 10 | "url": "http://localhost:3000/api/v1" 11 | } 12 | ], 13 | "tags": [ 14 | { 15 | "name": "Sheet", 16 | "description": "Managing spreadsheet" 17 | } 18 | ], 19 | "paths": { 20 | "/sheets": { 21 | "get": { 22 | "tags": ["Sheet"], 23 | "summary": "List all files", 24 | "description": "List down all the spreadsheets can be accessed by the service account.", 25 | "operationId": "getAllFileList", 26 | "parameters": [ 27 | { 28 | "name": "limit", 29 | "in": "query", 30 | "description": "Pagination page size", 31 | "required": false, 32 | "schema": { 33 | "type": "integer", 34 | "format": "int32", 35 | "default": 10, 36 | "example": 10 37 | } 38 | }, 39 | { 40 | "name": "next_token", 41 | "in": "query", 42 | "description": "Token to get next page", 43 | "required": false, 44 | "schema": { 45 | "type": "string" 46 | } 47 | } 48 | ], 49 | "responses": { 50 | "200": { 51 | "description": "Successful operation", 52 | "content": { 53 | "application/json": { 54 | "schema": { 55 | "type": "object", 56 | "properties": { 57 | "next_token": { 58 | "type": "string" 59 | }, 60 | "data": { 61 | "type": "array", 62 | "items": { 63 | "$ref": "#/components/schemas/Spreadsheet" 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | }, 71 | "400": { 72 | "description": "Empty data", 73 | "content": { 74 | "application/json": { 75 | "schema": { 76 | "type": "object", 77 | "properties": { 78 | "message": { 79 | "type": "string", 80 | "example": "Something bad happened!" 81 | }, 82 | "next_token": { 83 | "type": "string" 84 | }, 85 | "data": { 86 | "type": "array", 87 | "example": [] 88 | } 89 | } 90 | } 91 | } 92 | } 93 | }, 94 | "401": { 95 | "description": "Unauthorized", 96 | "content": { 97 | "application/json": { 98 | "schema": { 99 | "type": "object", 100 | "properties": { 101 | "message": { 102 | "type": "string", 103 | "example": "The API Key is invalid!" 104 | }, 105 | "next_token": { 106 | "type": "string" 107 | }, 108 | "data": { 109 | "type": "array", 110 | "example": [] 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | }, 118 | "security": [ 119 | { 120 | "x-api-key": [] 121 | } 122 | ] 123 | } 124 | }, 125 | "/sheets/{spreadsheet_id}": { 126 | "get": { 127 | "tags": ["Sheet"], 128 | "summary": "Get the sheet's info", 129 | "description": "Get the sheets available by using spreadsheet id", 130 | "operationId": "getSheetsBySpreadsheetId", 131 | "parameters": [ 132 | { 133 | "name": "spreadsheet_id", 134 | "in": "path", 135 | "description": "ID from the spreadsheet", 136 | "required": true, 137 | "schema": { 138 | "type": "string", 139 | "example": "1-Qi5_aizQiNTMRBuqboory9Ba7lyonxCjDCogASdVdg" 140 | } 141 | } 142 | ], 143 | "responses": { 144 | "200": { 145 | "description": "Successful operation", 146 | "content": { 147 | "application/json": { 148 | "schema": { 149 | "type": "object", 150 | "properties": { 151 | "data": { 152 | "type": "array", 153 | "items": { 154 | "$ref": "#/components/schemas/SheetInfo" 155 | } 156 | } 157 | } 158 | } 159 | } 160 | } 161 | }, 162 | "400": { 163 | "description": "Empty data", 164 | "content": { 165 | "application/json": { 166 | "schema": { 167 | "type": "object", 168 | "properties": { 169 | "message": { 170 | "type": "string", 171 | "example": "Something bad happened!" 172 | }, 173 | "data": { 174 | "type": "array", 175 | "example": [] 176 | } 177 | } 178 | } 179 | } 180 | } 181 | }, 182 | "401": { 183 | "description": "Unauthorized", 184 | "content": { 185 | "application/json": { 186 | "schema": { 187 | "type": "object", 188 | "properties": { 189 | "message": { 190 | "type": "string", 191 | "example": "The API Key is invalid!" 192 | }, 193 | "data": { 194 | "type": "array", 195 | "example": [] 196 | } 197 | } 198 | } 199 | } 200 | } 201 | } 202 | }, 203 | "security": [ 204 | { 205 | "x-api-key": [] 206 | } 207 | ] 208 | } 209 | }, 210 | "/sheets/{spreadsheet_id}/{sheet_name}": { 211 | "get": { 212 | "tags": ["Sheet"], 213 | "summary": "Get the sheet's data", 214 | "description": "Get the columns and the data with pagination.", 215 | "operationId": "getDataBySheetName", 216 | "parameters": [ 217 | { 218 | "name": "spreadsheet_id", 219 | "in": "path", 220 | "description": "ID from the spreadsheet", 221 | "required": true, 222 | "schema": { 223 | "type": "string", 224 | "example": "1-Qi5_aizQiNTMRBuqboory9Ba7lyonxCjDCogASdVdg" 225 | } 226 | }, 227 | { 228 | "name": "sheet_name", 229 | "in": "path", 230 | "description": "Title from the sheet", 231 | "required": true, 232 | "schema": { 233 | "type": "string", 234 | "example": "Sheet1" 235 | } 236 | }, 237 | { 238 | "name": "offset", 239 | "in": "query", 240 | "description": "Pagination offset", 241 | "required": false, 242 | "schema": { 243 | "type": "integer", 244 | "format": "int32", 245 | "default": 2, 246 | "example": 2 247 | } 248 | }, 249 | { 250 | "name": "limit", 251 | "in": "query", 252 | "description": "Pagination page size", 253 | "required": false, 254 | "schema": { 255 | "type": "integer", 256 | "format": "int32", 257 | "default": 100, 258 | "example": 100 259 | } 260 | }, 261 | { 262 | "name": "column_count", 263 | "in": "query", 264 | "description": "Maximum column number", 265 | "required": false, 266 | "schema": { 267 | "type": "integer", 268 | "format": "int32", 269 | "default": 10, 270 | "example": 10 271 | } 272 | } 273 | ], 274 | "responses": { 275 | "200": { 276 | "description": "Successful operation", 277 | "content": { 278 | "application/json": { 279 | "schema": { 280 | "type": "object", 281 | "properties": { 282 | "data": { 283 | "type": "array", 284 | "items": { 285 | "$ref": "#/components/schemas/SheetData" 286 | } 287 | }, 288 | "pagination": { 289 | "$ref": "#/components/schemas/SheetPagination" 290 | }, 291 | "columns": { 292 | "type": "array", 293 | "items": { 294 | "$ref": "#/components/schemas/SheetColumn" 295 | } 296 | } 297 | } 298 | } 299 | } 300 | } 301 | }, 302 | "400": { 303 | "description": "Empty data", 304 | "content": { 305 | "application/json": { 306 | "schema": { 307 | "type": "object", 308 | "properties": { 309 | "message": { 310 | "type": "string", 311 | "example": "Something bad happened!" 312 | }, 313 | "data": { 314 | "type": "array", 315 | "example": [] 316 | } 317 | } 318 | } 319 | } 320 | } 321 | }, 322 | "401": { 323 | "description": "Unauthorized", 324 | "content": { 325 | "application/json": { 326 | "schema": { 327 | "type": "object", 328 | "properties": { 329 | "message": { 330 | "type": "string", 331 | "example": "The API Key is invalid!" 332 | }, 333 | "data": { 334 | "type": "array", 335 | "example": [] 336 | } 337 | } 338 | } 339 | } 340 | } 341 | } 342 | }, 343 | "security": [ 344 | { 345 | "x-api-key": [] 346 | } 347 | ] 348 | }, 349 | "post": { 350 | "tags": ["Sheet"], 351 | "summary": "Append rows", 352 | "description": "Add new rows to the sheet.", 353 | "operationId": "appendSheetRow", 354 | "parameters": [ 355 | { 356 | "name": "spreadsheet_id", 357 | "in": "path", 358 | "description": "ID from the spreadsheet", 359 | "required": true, 360 | "schema": { 361 | "type": "string", 362 | "example": "1-Qi5_aizQiNTMRBuqboory9Ba7lyonxCjDCogASdVdg" 363 | } 364 | }, 365 | { 366 | "name": "sheet_name", 367 | "in": "path", 368 | "description": "Title from the sheet", 369 | "required": true, 370 | "schema": { 371 | "type": "string", 372 | "example": "Sheet1" 373 | } 374 | }, 375 | { 376 | "name": "column_count", 377 | "in": "query", 378 | "description": "Maximum column number", 379 | "required": false, 380 | "schema": { 381 | "type": "integer", 382 | "format": "int32", 383 | "default": 10, 384 | "example": 10 385 | } 386 | }, 387 | { 388 | "name": "input_options", 389 | "in": "query", 390 | "description": "Input options value", 391 | "required": false, 392 | "schema": { 393 | "type": "string", 394 | "default": "USER_ENTERED", 395 | "enum": ["USER_ENTERED", "RAW"] 396 | } 397 | } 398 | ], 399 | "requestBody": { 400 | "content": { 401 | "application/json": { 402 | "schema": { 403 | "type": "object", 404 | "properties": { 405 | "data": { 406 | "type": "array", 407 | "example": [{ "name": "Jean", "email": "jean@appleseed.com" }, { "name": "Bunny", "email": "bunny@appleseed.com" }] 408 | } 409 | } 410 | } 411 | } 412 | }, 413 | "required": true 414 | }, 415 | "responses": { 416 | "200": { 417 | "description": "Successful operation", 418 | "content": { 419 | "application/json": { 420 | "schema": { 421 | "type": "object", 422 | "properties": { 423 | "data": { 424 | "type": "array", 425 | "items": { 426 | "$ref": "#/components/schemas/UpdateSheetResponse" 427 | } 428 | } 429 | } 430 | } 431 | } 432 | } 433 | }, 434 | "400": { 435 | "description": "Empty data", 436 | "content": { 437 | "application/json": { 438 | "schema": { 439 | "type": "object", 440 | "properties": { 441 | "message": { 442 | "type": "string", 443 | "example": "Something bad happened!" 444 | }, 445 | "data": { 446 | "type": "array", 447 | "example": [] 448 | } 449 | } 450 | } 451 | } 452 | } 453 | }, 454 | "401": { 455 | "description": "Unauthorized", 456 | "content": { 457 | "application/json": { 458 | "schema": { 459 | "type": "object", 460 | "properties": { 461 | "message": { 462 | "type": "string", 463 | "example": "The API Key is invalid!" 464 | }, 465 | "data": { 466 | "type": "array", 467 | "example": [] 468 | } 469 | } 470 | } 471 | } 472 | } 473 | } 474 | }, 475 | "security": [ 476 | { 477 | "x-api-key": [] 478 | } 479 | ] 480 | }, 481 | "put": { 482 | "tags": ["Sheet"], 483 | "summary": "Update rows", 484 | "description": "Update certain rows inside sheet.", 485 | "operationId": "updateSheetRow", 486 | "parameters": [ 487 | { 488 | "name": "spreadsheet_id", 489 | "in": "path", 490 | "description": "ID from the spreadsheet", 491 | "required": true, 492 | "schema": { 493 | "type": "string", 494 | "example": "1-Qi5_aizQiNTMRBuqboory9Ba7lyonxCjDCogASdVdg" 495 | } 496 | }, 497 | { 498 | "name": "sheet_name", 499 | "in": "path", 500 | "description": "Title from the sheet", 501 | "required": true, 502 | "schema": { 503 | "type": "string", 504 | "example": "Sheet1" 505 | } 506 | }, 507 | { 508 | "name": "column_count", 509 | "in": "query", 510 | "description": "Maximum column number", 511 | "required": false, 512 | "schema": { 513 | "type": "integer", 514 | "format": "int32", 515 | "default": 10, 516 | "example": 10 517 | } 518 | }, 519 | { 520 | "name": "input_options", 521 | "in": "query", 522 | "description": "Input options value", 523 | "required": false, 524 | "schema": { 525 | "type": "string", 526 | "default": "USER_ENTERED", 527 | "enum": ["USER_ENTERED", "RAW"] 528 | } 529 | } 530 | ], 531 | "requestBody": { 532 | "content": { 533 | "application/json": { 534 | "schema": { 535 | "type": "object", 536 | "example": { 537 | "4": { "email": "john@appleseed.com" }, 538 | "1": { "phone": "415-500-7000" } 539 | } 540 | } 541 | } 542 | }, 543 | "required": true 544 | }, 545 | "responses": { 546 | "200": { 547 | "description": "Successful operation", 548 | "content": { 549 | "application/json": { 550 | "schema": { 551 | "type": "object", 552 | "properties": { 553 | "data": { 554 | "type": "array", 555 | "items": { 556 | "$ref": "#/components/schemas/UpdateSheetResponse" 557 | } 558 | } 559 | } 560 | } 561 | } 562 | } 563 | }, 564 | "400": { 565 | "description": "Empty data", 566 | "content": { 567 | "application/json": { 568 | "schema": { 569 | "type": "object", 570 | "properties": { 571 | "message": { 572 | "type": "string", 573 | "example": "Something bad happened!" 574 | }, 575 | "data": { 576 | "type": "array", 577 | "example": [] 578 | } 579 | } 580 | } 581 | } 582 | } 583 | }, 584 | "401": { 585 | "description": "Unauthorized", 586 | "content": { 587 | "application/json": { 588 | "schema": { 589 | "type": "object", 590 | "properties": { 591 | "message": { 592 | "type": "string", 593 | "example": "The API Key is invalid!" 594 | }, 595 | "data": { 596 | "type": "array", 597 | "example": [] 598 | } 599 | } 600 | } 601 | } 602 | } 603 | } 604 | }, 605 | "security": [ 606 | { 607 | "x-api-key": [] 608 | } 609 | ] 610 | } 611 | }, 612 | "/sheets/{spreadsheet_id}/{sheet_name}/{row}": { 613 | "get": { 614 | "tags": ["Sheet"], 615 | "summary": "Get the row data", 616 | "description": "Get data in certain row inside sheet.", 617 | "operationId": "getSheetDataByRows", 618 | "parameters": [ 619 | { 620 | "name": "spreadsheet_id", 621 | "in": "path", 622 | "description": "ID from the spreadsheet", 623 | "required": true, 624 | "schema": { 625 | "type": "string", 626 | "example": "1-Qi5_aizQiNTMRBuqboory9Ba7lyonxCjDCogASdVdg" 627 | } 628 | }, 629 | { 630 | "name": "sheet_name", 631 | "in": "path", 632 | "description": "Title from the sheet", 633 | "required": true, 634 | "schema": { 635 | "type": "string", 636 | "example": "Sheet1" 637 | } 638 | }, 639 | { 640 | "name": "row", 641 | "in": "path", 642 | "description": "Row number", 643 | "required": true, 644 | "schema": { 645 | "type": "integer", 646 | "format": "int32", 647 | "example": 2 648 | } 649 | }, 650 | { 651 | "name": "column_count", 652 | "in": "query", 653 | "description": "Maximum column number", 654 | "required": false, 655 | "schema": { 656 | "type": "integer", 657 | "format": "int32", 658 | "default": 10, 659 | "example": 10 660 | } 661 | } 662 | ], 663 | "responses": { 664 | "200": { 665 | "description": "Successful operation", 666 | "content": { 667 | "application/json": { 668 | "schema": { 669 | "type": "object", 670 | "properties": { 671 | "data": { 672 | "$ref": "#/components/schemas/SheetData" 673 | } 674 | } 675 | } 676 | } 677 | } 678 | }, 679 | "400": { 680 | "description": "Empty data", 681 | "content": { 682 | "application/json": { 683 | "schema": { 684 | "type": "object", 685 | "properties": { 686 | "message": { 687 | "type": "string", 688 | "example": "Something bad happened!" 689 | }, 690 | "data": { 691 | "type": "object" 692 | } 693 | } 694 | } 695 | } 696 | } 697 | }, 698 | "401": { 699 | "description": "Unauthorized", 700 | "content": { 701 | "application/json": { 702 | "schema": { 703 | "type": "object", 704 | "properties": { 705 | "message": { 706 | "type": "string", 707 | "example": "The API Key is invalid!" 708 | }, 709 | "data": { 710 | "type": "object" 711 | } 712 | } 713 | } 714 | } 715 | } 716 | } 717 | }, 718 | "security": [ 719 | { 720 | "x-api-key": [] 721 | } 722 | ] 723 | }, 724 | "delete": { 725 | "tags": ["Sheet"], 726 | "summary": "Remove rows", 727 | "description": "Remove certain rows from sheet. Multiple rows using comma separator.", 728 | "operationId": "removeSheetRows", 729 | "parameters": [ 730 | { 731 | "name": "spreadsheet_id", 732 | "in": "path", 733 | "description": "ID from the spreadsheet", 734 | "required": true, 735 | "schema": { 736 | "type": "string", 737 | "example": "1-Qi5_aizQiNTMRBuqboory9Ba7lyonxCjDCogASdVdg" 738 | } 739 | }, 740 | { 741 | "name": "sheet_name", 742 | "in": "path", 743 | "description": "Title from the sheet", 744 | "required": true, 745 | "schema": { 746 | "type": "string", 747 | "example": "Sheet1" 748 | } 749 | }, 750 | { 751 | "name": "row", 752 | "in": "path", 753 | "description": "Rows to be deleted. Use comma to remove multiple rows.", 754 | "required": true, 755 | "schema": { 756 | "type": "string", 757 | "example": "1,2,3" 758 | } 759 | } 760 | ], 761 | "responses": { 762 | "200": { 763 | "description": "Successful operation", 764 | "content": { 765 | "application/json": { 766 | "schema": { 767 | "type": "object", 768 | "properties": { 769 | "deleted_rows": { 770 | "type": "integer", 771 | "format": "int32", 772 | "example": 3 773 | } 774 | } 775 | } 776 | } 777 | } 778 | }, 779 | "400": { 780 | "description": "Empty data", 781 | "content": { 782 | "application/json": { 783 | "schema": { 784 | "type": "object", 785 | "properties": { 786 | "message": { 787 | "type": "string", 788 | "example": "Something bad happened!" 789 | }, 790 | "deleted_rows": { 791 | "type": "integer", 792 | "format": "int32", 793 | "example": 0 794 | } 795 | } 796 | } 797 | } 798 | } 799 | }, 800 | "401": { 801 | "description": "Unauthorized", 802 | "content": { 803 | "application/json": { 804 | "schema": { 805 | "type": "object", 806 | "properties": { 807 | "message": { 808 | "type": "string", 809 | "example": "The API Key is invalid!" 810 | }, 811 | "deleted_rows": { 812 | "type": "integer", 813 | "format": "int32", 814 | "example": 0 815 | } 816 | } 817 | } 818 | } 819 | } 820 | } 821 | }, 822 | "security": [ 823 | { 824 | "x-api-key": [] 825 | } 826 | ] 827 | } 828 | } 829 | }, 830 | "components": { 831 | "schemas": { 832 | "Spreadsheet": { 833 | "type": "object", 834 | "properties": { 835 | "id": { 836 | "type": "string", 837 | "example": "1-Qi5_aizQiNTMRBuqboory9Ba7lyonxCjDCogASdVdg" 838 | }, 839 | "name": { 840 | "type": "string", 841 | "example": "sample-spreadsheet" 842 | }, 843 | "modifiedTime": { 844 | "type": "string", 845 | "format": "date-time", 846 | "example": "2025-02-03T08:58:19.749Z" 847 | } 848 | } 849 | }, 850 | "SheetInfo": { 851 | "type": "object", 852 | "properties": { 853 | "title": { 854 | "type": "string", 855 | "example": "Sheet1" 856 | }, 857 | "index": { 858 | "type": "integer", 859 | "format": "int32", 860 | "example": 0 861 | }, 862 | "sheetId": { 863 | "type": "integer", 864 | "format": "int32", 865 | "example": 1 866 | }, 867 | "rowCount": { 868 | "type": "integer", 869 | "format": "int32", 870 | "example": 1000 871 | }, 872 | "columnCount": { 873 | "type": "integer", 874 | "format": "int32", 875 | "example": 10 876 | } 877 | } 878 | }, 879 | "SheetData": { 880 | "type": "object", 881 | "example": { 882 | "_row": 2, 883 | "NO": 1, 884 | "SLUG": "test", 885 | "VIEWS": 100, 886 | "LIKE": 1000 887 | }, 888 | "properties": { 889 | "_row": { 890 | "type": "integer", 891 | "format": "int32", 892 | "example": 0 893 | } 894 | } 895 | }, 896 | "SheetPagination": { 897 | "type": "object", 898 | "properties": { 899 | "limit": { 900 | "type": "integer", 901 | "format": "int32", 902 | "example": 20 903 | }, 904 | "cell_range": { 905 | "type": "string", 906 | "example": "Sheet1!A2:J101" 907 | }, 908 | "offset": { 909 | "type": "integer", 910 | "format": "int32", 911 | "example": 0 912 | }, 913 | "next_offset": { 914 | "type": "integer", 915 | "format": "int32", 916 | "example": 21 917 | }, 918 | "total": { 919 | "type": "integer", 920 | "format": "int32", 921 | "example": 100 922 | }, 923 | "hasNext": { 924 | "type": "boolean" 925 | } 926 | } 927 | }, 928 | "SheetColumn": { 929 | "type": "object", 930 | "properties": { 931 | "title": { 932 | "type": "string", 933 | "example": "Title" 934 | }, 935 | "cell": { 936 | "type": "string", 937 | "example": "Sheet1!A" 938 | } 939 | } 940 | }, 941 | "UpdateSheetResponse": { 942 | "type": "object", 943 | "properties": { 944 | "spreadsheetId": { 945 | "type": "string", 946 | "example": "1-Qi5_aizQiNTMRBuqboory9Ba7lyonxCjDCogASdVdg" 947 | }, 948 | "updatedCells": { 949 | "type": "integer", 950 | "format": "int32", 951 | "example": 1 952 | }, 953 | "updatedColumns": { 954 | "type": "integer", 955 | "format": "int32", 956 | "example": 1 957 | }, 958 | "updatedRows": { 959 | "type": "integer", 960 | "format": "int32", 961 | "example": 1 962 | }, 963 | "updatedRange": { 964 | "type": "string", 965 | "example": "Sheet1!B4" 966 | } 967 | } 968 | } 969 | }, 970 | "requestBodies": {}, 971 | "securitySchemes": { 972 | "x-api-key": { 973 | "type": "apiKey", 974 | "name": "x-api-key", 975 | "in": "header" 976 | } 977 | } 978 | } 979 | } 980 | -------------------------------------------------------------------------------- /public/gsheet-rest-api.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "09eaf3d9-2b4f-4b1f-8463-1e3edecb928d", 4 | "name": "GSheet Rest API", 5 | "description": "Simple yet deployable rest API for your Google Sheet. Turn your Google Sheet into API. \n\n [![GitHub Repo stars](https://img.shields.io/github/stars/mazipan/gsheet-rest-api)](https://github.com/mazipan/gsheet-rest-api) ![GitHub forks](https://img.shields.io/github/forks/mazipan/gsheet-rest-api) \n\n [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fmazipan%2Fgsheet-rest-api)", 6 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 7 | "_exporter_id": "409435" 8 | }, 9 | "item": [ 10 | { 11 | "name": "sheets", 12 | "item": [ 13 | { 14 | "name": "{spreadsheet_id}", 15 | "item": [ 16 | { 17 | "name": "{sheet_name}", 18 | "item": [ 19 | { 20 | "name": "{row}", 21 | "item": [ 22 | { 23 | "name": "Get the row data", 24 | "request": { 25 | "method": "GET", 26 | "header": [ 27 | { 28 | "key": "Accept", 29 | "value": "application/json" 30 | } 31 | ], 32 | "url": { 33 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id/:sheet_name/:row?column_count=10", 34 | "host": [ 35 | "{{baseUrl}}" 36 | ], 37 | "path": [ 38 | "sheets", 39 | ":spreadsheet_id", 40 | ":sheet_name", 41 | ":row" 42 | ], 43 | "query": [ 44 | { 45 | "key": "column_count", 46 | "value": "10", 47 | "description": "Maximum column number" 48 | } 49 | ], 50 | "variable": [ 51 | { 52 | "key": "spreadsheet_id", 53 | "value": "", 54 | "description": "(Required) ID from the spreadsheet" 55 | }, 56 | { 57 | "key": "sheet_name", 58 | "value": "", 59 | "description": "(Required) Title from the sheet" 60 | }, 61 | { 62 | "key": "row", 63 | "value": "", 64 | "description": "(Required) Row number" 65 | } 66 | ] 67 | }, 68 | "description": "Get data in certain row inside sheet." 69 | }, 70 | "response": [ 71 | { 72 | "name": "Successful operation", 73 | "originalRequest": { 74 | "method": "GET", 75 | "header": [ 76 | { 77 | "key": "Accept", 78 | "value": "application/json" 79 | } 80 | ], 81 | "url": { 82 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id/:sheet_name/:row?column_count=10", 83 | "host": [ 84 | "{{baseUrl}}" 85 | ], 86 | "path": [ 87 | "sheets", 88 | ":spreadsheet_id", 89 | ":sheet_name", 90 | ":row" 91 | ], 92 | "query": [ 93 | { 94 | "key": "column_count", 95 | "value": "10", 96 | "description": "Maximum column number" 97 | } 98 | ], 99 | "variable": [ 100 | { 101 | "key": "spreadsheet_id" 102 | }, 103 | { 104 | "key": "sheet_name" 105 | }, 106 | { 107 | "key": "row" 108 | } 109 | ] 110 | } 111 | }, 112 | "status": "OK", 113 | "code": 200, 114 | "_postman_previewlanguage": "json", 115 | "header": [ 116 | { 117 | "key": "Content-Type", 118 | "value": "application/json" 119 | } 120 | ], 121 | "cookie": [], 122 | "body": "{\n \"data\": {\n \"_row\": \"\"\n }\n}" 123 | }, 124 | { 125 | "name": "Empty data", 126 | "originalRequest": { 127 | "method": "GET", 128 | "header": [ 129 | { 130 | "key": "Accept", 131 | "value": "application/json" 132 | } 133 | ], 134 | "url": { 135 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id/:sheet_name/:row?column_count=10", 136 | "host": [ 137 | "{{baseUrl}}" 138 | ], 139 | "path": [ 140 | "sheets", 141 | ":spreadsheet_id", 142 | ":sheet_name", 143 | ":row" 144 | ], 145 | "query": [ 146 | { 147 | "key": "column_count", 148 | "value": "10", 149 | "description": "Maximum column number" 150 | } 151 | ], 152 | "variable": [ 153 | { 154 | "key": "spreadsheet_id" 155 | }, 156 | { 157 | "key": "sheet_name" 158 | }, 159 | { 160 | "key": "row" 161 | } 162 | ] 163 | } 164 | }, 165 | "status": "Bad Request", 166 | "code": 400, 167 | "_postman_previewlanguage": "json", 168 | "header": [ 169 | { 170 | "key": "Content-Type", 171 | "value": "application/json" 172 | } 173 | ], 174 | "cookie": [], 175 | "body": "{\n \"message\": \"\",\n \"data\": {}\n}" 176 | } 177 | ] 178 | }, 179 | { 180 | "name": "Remove rows", 181 | "request": { 182 | "method": "DELETE", 183 | "header": [ 184 | { 185 | "key": "Accept", 186 | "value": "application/json" 187 | } 188 | ], 189 | "url": { 190 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id/:sheet_name/:row", 191 | "host": [ 192 | "{{baseUrl}}" 193 | ], 194 | "path": [ 195 | "sheets", 196 | ":spreadsheet_id", 197 | ":sheet_name", 198 | ":row" 199 | ], 200 | "variable": [ 201 | { 202 | "key": "spreadsheet_id", 203 | "value": "", 204 | "description": "(Required) ID from the spreadsheet" 205 | }, 206 | { 207 | "key": "sheet_name", 208 | "value": "", 209 | "description": "(Required) Title from the sheet" 210 | }, 211 | { 212 | "key": "row", 213 | "value": "", 214 | "description": "(Required) Rows to be deleted. Use comma to remove multiple rows." 215 | } 216 | ] 217 | }, 218 | "description": "Remove certain rows from sheet. Multiple rows using comma separator." 219 | }, 220 | "response": [ 221 | { 222 | "name": "Successful operation", 223 | "originalRequest": { 224 | "method": "DELETE", 225 | "header": [ 226 | { 227 | "key": "Accept", 228 | "value": "application/json" 229 | } 230 | ], 231 | "url": { 232 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id/:sheet_name/:row", 233 | "host": [ 234 | "{{baseUrl}}" 235 | ], 236 | "path": [ 237 | "sheets", 238 | ":spreadsheet_id", 239 | ":sheet_name", 240 | ":row" 241 | ], 242 | "variable": [ 243 | { 244 | "key": "spreadsheet_id" 245 | }, 246 | { 247 | "key": "sheet_name" 248 | }, 249 | { 250 | "key": "row" 251 | } 252 | ] 253 | } 254 | }, 255 | "status": "OK", 256 | "code": 200, 257 | "_postman_previewlanguage": "json", 258 | "header": [ 259 | { 260 | "key": "Content-Type", 261 | "value": "application/json" 262 | } 263 | ], 264 | "cookie": [], 265 | "body": "{\n \"deleted_rows\": \"\"\n}" 266 | }, 267 | { 268 | "name": "Empty data", 269 | "originalRequest": { 270 | "method": "DELETE", 271 | "header": [ 272 | { 273 | "key": "Accept", 274 | "value": "application/json" 275 | } 276 | ], 277 | "url": { 278 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id/:sheet_name/:row", 279 | "host": [ 280 | "{{baseUrl}}" 281 | ], 282 | "path": [ 283 | "sheets", 284 | ":spreadsheet_id", 285 | ":sheet_name", 286 | ":row" 287 | ], 288 | "variable": [ 289 | { 290 | "key": "spreadsheet_id" 291 | }, 292 | { 293 | "key": "sheet_name" 294 | }, 295 | { 296 | "key": "row" 297 | } 298 | ] 299 | } 300 | }, 301 | "status": "Bad Request", 302 | "code": 400, 303 | "_postman_previewlanguage": "json", 304 | "header": [ 305 | { 306 | "key": "Content-Type", 307 | "value": "application/json" 308 | } 309 | ], 310 | "cookie": [], 311 | "body": "{\n \"message\": \"\",\n \"deleted_rows\": \"\"\n}" 312 | } 313 | ] 314 | } 315 | ] 316 | }, 317 | { 318 | "name": "Get the sheet's data", 319 | "request": { 320 | "method": "GET", 321 | "header": [ 322 | { 323 | "key": "Accept", 324 | "value": "application/json" 325 | } 326 | ], 327 | "url": { 328 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id/:sheet_name?offset=2&limit=100&column_count=10", 329 | "host": [ 330 | "{{baseUrl}}" 331 | ], 332 | "path": [ 333 | "sheets", 334 | ":spreadsheet_id", 335 | ":sheet_name" 336 | ], 337 | "query": [ 338 | { 339 | "key": "offset", 340 | "value": "2", 341 | "description": "Pagination offset" 342 | }, 343 | { 344 | "key": "limit", 345 | "value": "100", 346 | "description": "Pagination page size" 347 | }, 348 | { 349 | "key": "column_count", 350 | "value": "10", 351 | "description": "Maximum column number" 352 | } 353 | ], 354 | "variable": [ 355 | { 356 | "key": "spreadsheet_id", 357 | "value": "", 358 | "description": "(Required) ID from the spreadsheet" 359 | }, 360 | { 361 | "key": "sheet_name", 362 | "value": "", 363 | "description": "(Required) Title from the sheet" 364 | } 365 | ] 366 | }, 367 | "description": "Get the columns and the data with pagination." 368 | }, 369 | "response": [ 370 | { 371 | "name": "Successful operation", 372 | "originalRequest": { 373 | "method": "GET", 374 | "header": [ 375 | { 376 | "key": "Accept", 377 | "value": "application/json" 378 | } 379 | ], 380 | "url": { 381 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id/:sheet_name?offset=2&limit=100&column_count=10", 382 | "host": [ 383 | "{{baseUrl}}" 384 | ], 385 | "path": [ 386 | "sheets", 387 | ":spreadsheet_id", 388 | ":sheet_name" 389 | ], 390 | "query": [ 391 | { 392 | "key": "offset", 393 | "value": "2", 394 | "description": "Pagination offset" 395 | }, 396 | { 397 | "key": "limit", 398 | "value": "100", 399 | "description": "Pagination page size" 400 | }, 401 | { 402 | "key": "column_count", 403 | "value": "10", 404 | "description": "Maximum column number" 405 | } 406 | ], 407 | "variable": [ 408 | { 409 | "key": "spreadsheet_id" 410 | }, 411 | { 412 | "key": "sheet_name" 413 | } 414 | ] 415 | } 416 | }, 417 | "status": "OK", 418 | "code": 200, 419 | "_postman_previewlanguage": "json", 420 | "header": [ 421 | { 422 | "key": "Content-Type", 423 | "value": "application/json" 424 | } 425 | ], 426 | "cookie": [], 427 | "body": "{\n \"data\": [\n {\n \"_row\": \"\"\n },\n {\n \"_row\": \"\"\n }\n ],\n \"pagination\": {\n \"limit\": \"\",\n \"cell_range\": \"\",\n \"offset\": \"\",\n \"next_offset\": \"\",\n \"total\": \"\",\n \"hasNext\": \"\"\n },\n \"columns\": [\n {\n \"title\": \"\",\n \"cell\": \"\"\n },\n {\n \"title\": \"\",\n \"cell\": \"\"\n }\n ]\n}" 428 | }, 429 | { 430 | "name": "Empty data", 431 | "originalRequest": { 432 | "method": "GET", 433 | "header": [ 434 | { 435 | "key": "Accept", 436 | "value": "application/json" 437 | } 438 | ], 439 | "url": { 440 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id/:sheet_name?offset=2&limit=100&column_count=10", 441 | "host": [ 442 | "{{baseUrl}}" 443 | ], 444 | "path": [ 445 | "sheets", 446 | ":spreadsheet_id", 447 | ":sheet_name" 448 | ], 449 | "query": [ 450 | { 451 | "key": "offset", 452 | "value": "2", 453 | "description": "Pagination offset" 454 | }, 455 | { 456 | "key": "limit", 457 | "value": "100", 458 | "description": "Pagination page size" 459 | }, 460 | { 461 | "key": "column_count", 462 | "value": "10", 463 | "description": "Maximum column number" 464 | } 465 | ], 466 | "variable": [ 467 | { 468 | "key": "spreadsheet_id" 469 | }, 470 | { 471 | "key": "sheet_name" 472 | } 473 | ] 474 | } 475 | }, 476 | "status": "Bad Request", 477 | "code": 400, 478 | "_postman_previewlanguage": "json", 479 | "header": [ 480 | { 481 | "key": "Content-Type", 482 | "value": "application/json" 483 | } 484 | ], 485 | "cookie": [], 486 | "body": "{\n \"message\": \"\",\n \"data\": \"\"\n}" 487 | } 488 | ] 489 | }, 490 | { 491 | "name": "Append rows", 492 | "request": { 493 | "method": "POST", 494 | "header": [ 495 | { 496 | "key": "Content-Type", 497 | "value": "application/json" 498 | }, 499 | { 500 | "key": "Accept", 501 | "value": "application/json" 502 | } 503 | ], 504 | "body": { 505 | "mode": "raw", 506 | "raw": "{\n \"data\": \"\"\n}", 507 | "options": { 508 | "raw": { 509 | "headerFamily": "json", 510 | "language": "json" 511 | } 512 | } 513 | }, 514 | "url": { 515 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id/:sheet_name?column_count=10&input_options=USER_ENTERED", 516 | "host": [ 517 | "{{baseUrl}}" 518 | ], 519 | "path": [ 520 | "sheets", 521 | ":spreadsheet_id", 522 | ":sheet_name" 523 | ], 524 | "query": [ 525 | { 526 | "key": "column_count", 527 | "value": "10", 528 | "description": "Maximum column number" 529 | }, 530 | { 531 | "key": "input_options", 532 | "value": "USER_ENTERED", 533 | "description": "Input options value" 534 | } 535 | ], 536 | "variable": [ 537 | { 538 | "key": "spreadsheet_id", 539 | "value": "", 540 | "description": "(Required) ID from the spreadsheet" 541 | }, 542 | { 543 | "key": "sheet_name", 544 | "value": "", 545 | "description": "(Required) Title from the sheet" 546 | } 547 | ] 548 | }, 549 | "description": "Add new rows to the sheet." 550 | }, 551 | "response": [ 552 | { 553 | "name": "Successful operation", 554 | "originalRequest": { 555 | "method": "POST", 556 | "header": [ 557 | { 558 | "key": "Content-Type", 559 | "value": "application/json" 560 | }, 561 | { 562 | "key": "Accept", 563 | "value": "application/json" 564 | } 565 | ], 566 | "body": { 567 | "mode": "raw", 568 | "raw": "{\n \"data\": \"\"\n}", 569 | "options": { 570 | "raw": { 571 | "headerFamily": "json", 572 | "language": "json" 573 | } 574 | } 575 | }, 576 | "url": { 577 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id/:sheet_name?column_count=10&input_options=USER_ENTERED", 578 | "host": [ 579 | "{{baseUrl}}" 580 | ], 581 | "path": [ 582 | "sheets", 583 | ":spreadsheet_id", 584 | ":sheet_name" 585 | ], 586 | "query": [ 587 | { 588 | "key": "column_count", 589 | "value": "10", 590 | "description": "Maximum column number" 591 | }, 592 | { 593 | "key": "input_options", 594 | "value": "USER_ENTERED", 595 | "description": "Input options value" 596 | } 597 | ], 598 | "variable": [ 599 | { 600 | "key": "spreadsheet_id" 601 | }, 602 | { 603 | "key": "sheet_name" 604 | } 605 | ] 606 | } 607 | }, 608 | "status": "OK", 609 | "code": 200, 610 | "_postman_previewlanguage": "json", 611 | "header": [ 612 | { 613 | "key": "Content-Type", 614 | "value": "application/json" 615 | } 616 | ], 617 | "cookie": [], 618 | "body": "{\n \"data\": [\n {\n \"spreadsheetId\": \"\",\n \"updatedCells\": \"\",\n \"updatedColumns\": \"\",\n \"updatedRows\": \"\",\n \"updatedRange\": \"\"\n },\n {\n \"spreadsheetId\": \"\",\n \"updatedCells\": \"\",\n \"updatedColumns\": \"\",\n \"updatedRows\": \"\",\n \"updatedRange\": \"\"\n }\n ]\n}" 619 | }, 620 | { 621 | "name": "Empty data", 622 | "originalRequest": { 623 | "method": "POST", 624 | "header": [ 625 | { 626 | "key": "Content-Type", 627 | "value": "application/json" 628 | }, 629 | { 630 | "key": "Accept", 631 | "value": "application/json" 632 | } 633 | ], 634 | "body": { 635 | "mode": "raw", 636 | "raw": "{\n \"data\": \"\"\n}", 637 | "options": { 638 | "raw": { 639 | "headerFamily": "json", 640 | "language": "json" 641 | } 642 | } 643 | }, 644 | "url": { 645 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id/:sheet_name?column_count=10&input_options=USER_ENTERED", 646 | "host": [ 647 | "{{baseUrl}}" 648 | ], 649 | "path": [ 650 | "sheets", 651 | ":spreadsheet_id", 652 | ":sheet_name" 653 | ], 654 | "query": [ 655 | { 656 | "key": "column_count", 657 | "value": "10", 658 | "description": "Maximum column number" 659 | }, 660 | { 661 | "key": "input_options", 662 | "value": "USER_ENTERED", 663 | "description": "Input options value" 664 | } 665 | ], 666 | "variable": [ 667 | { 668 | "key": "spreadsheet_id" 669 | }, 670 | { 671 | "key": "sheet_name" 672 | } 673 | ] 674 | } 675 | }, 676 | "status": "Bad Request", 677 | "code": 400, 678 | "_postman_previewlanguage": "json", 679 | "header": [ 680 | { 681 | "key": "Content-Type", 682 | "value": "application/json" 683 | } 684 | ], 685 | "cookie": [], 686 | "body": "{\n \"message\": \"\",\n \"data\": \"\"\n}" 687 | } 688 | ] 689 | }, 690 | { 691 | "name": "Update rows", 692 | "request": { 693 | "method": "PUT", 694 | "header": [ 695 | { 696 | "key": "Content-Type", 697 | "value": "application/json" 698 | }, 699 | { 700 | "key": "Accept", 701 | "value": "application/json" 702 | } 703 | ], 704 | "body": { 705 | "mode": "raw", 706 | "raw": "{}", 707 | "options": { 708 | "raw": { 709 | "headerFamily": "json", 710 | "language": "json" 711 | } 712 | } 713 | }, 714 | "url": { 715 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id/:sheet_name?column_count=10&input_options=USER_ENTERED", 716 | "host": [ 717 | "{{baseUrl}}" 718 | ], 719 | "path": [ 720 | "sheets", 721 | ":spreadsheet_id", 722 | ":sheet_name" 723 | ], 724 | "query": [ 725 | { 726 | "key": "column_count", 727 | "value": "10", 728 | "description": "Maximum column number" 729 | }, 730 | { 731 | "key": "input_options", 732 | "value": "USER_ENTERED", 733 | "description": "Input options value" 734 | } 735 | ], 736 | "variable": [ 737 | { 738 | "key": "spreadsheet_id", 739 | "value": "", 740 | "description": "(Required) ID from the spreadsheet" 741 | }, 742 | { 743 | "key": "sheet_name", 744 | "value": "", 745 | "description": "(Required) Title from the sheet" 746 | } 747 | ] 748 | }, 749 | "description": "Update certain rows inside sheet." 750 | }, 751 | "response": [ 752 | { 753 | "name": "Successful operation", 754 | "originalRequest": { 755 | "method": "PUT", 756 | "header": [ 757 | { 758 | "key": "Content-Type", 759 | "value": "application/json" 760 | }, 761 | { 762 | "key": "Accept", 763 | "value": "application/json" 764 | } 765 | ], 766 | "body": { 767 | "mode": "raw", 768 | "raw": "{}", 769 | "options": { 770 | "raw": { 771 | "headerFamily": "json", 772 | "language": "json" 773 | } 774 | } 775 | }, 776 | "url": { 777 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id/:sheet_name?column_count=10&input_options=USER_ENTERED", 778 | "host": [ 779 | "{{baseUrl}}" 780 | ], 781 | "path": [ 782 | "sheets", 783 | ":spreadsheet_id", 784 | ":sheet_name" 785 | ], 786 | "query": [ 787 | { 788 | "key": "column_count", 789 | "value": "10", 790 | "description": "Maximum column number" 791 | }, 792 | { 793 | "key": "input_options", 794 | "value": "USER_ENTERED", 795 | "description": "Input options value" 796 | } 797 | ], 798 | "variable": [ 799 | { 800 | "key": "spreadsheet_id" 801 | }, 802 | { 803 | "key": "sheet_name" 804 | } 805 | ] 806 | } 807 | }, 808 | "status": "OK", 809 | "code": 200, 810 | "_postman_previewlanguage": "json", 811 | "header": [ 812 | { 813 | "key": "Content-Type", 814 | "value": "application/json" 815 | } 816 | ], 817 | "cookie": [], 818 | "body": "{\n \"data\": [\n {\n \"spreadsheetId\": \"\",\n \"updatedCells\": \"\",\n \"updatedColumns\": \"\",\n \"updatedRows\": \"\",\n \"updatedRange\": \"\"\n },\n {\n \"spreadsheetId\": \"\",\n \"updatedCells\": \"\",\n \"updatedColumns\": \"\",\n \"updatedRows\": \"\",\n \"updatedRange\": \"\"\n }\n ]\n}" 819 | }, 820 | { 821 | "name": "Empty data", 822 | "originalRequest": { 823 | "method": "PUT", 824 | "header": [ 825 | { 826 | "key": "Content-Type", 827 | "value": "application/json" 828 | }, 829 | { 830 | "key": "Accept", 831 | "value": "application/json" 832 | } 833 | ], 834 | "body": { 835 | "mode": "raw", 836 | "raw": "{}", 837 | "options": { 838 | "raw": { 839 | "headerFamily": "json", 840 | "language": "json" 841 | } 842 | } 843 | }, 844 | "url": { 845 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id/:sheet_name?column_count=10&input_options=USER_ENTERED", 846 | "host": [ 847 | "{{baseUrl}}" 848 | ], 849 | "path": [ 850 | "sheets", 851 | ":spreadsheet_id", 852 | ":sheet_name" 853 | ], 854 | "query": [ 855 | { 856 | "key": "column_count", 857 | "value": "10", 858 | "description": "Maximum column number" 859 | }, 860 | { 861 | "key": "input_options", 862 | "value": "USER_ENTERED", 863 | "description": "Input options value" 864 | } 865 | ], 866 | "variable": [ 867 | { 868 | "key": "spreadsheet_id" 869 | }, 870 | { 871 | "key": "sheet_name" 872 | } 873 | ] 874 | } 875 | }, 876 | "status": "Bad Request", 877 | "code": 400, 878 | "_postman_previewlanguage": "json", 879 | "header": [ 880 | { 881 | "key": "Content-Type", 882 | "value": "application/json" 883 | } 884 | ], 885 | "cookie": [], 886 | "body": "{\n \"message\": \"\",\n \"data\": \"\"\n}" 887 | } 888 | ] 889 | } 890 | ] 891 | }, 892 | { 893 | "name": "Get the sheet's info", 894 | "request": { 895 | "method": "GET", 896 | "header": [ 897 | { 898 | "key": "Accept", 899 | "value": "application/json" 900 | } 901 | ], 902 | "url": { 903 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id", 904 | "host": [ 905 | "{{baseUrl}}" 906 | ], 907 | "path": [ 908 | "sheets", 909 | ":spreadsheet_id" 910 | ], 911 | "variable": [ 912 | { 913 | "key": "spreadsheet_id", 914 | "value": "", 915 | "description": "(Required) ID from the spreadsheet" 916 | } 917 | ] 918 | }, 919 | "description": "Get the sheets available by using spreadsheet id" 920 | }, 921 | "response": [ 922 | { 923 | "name": "Successful operation", 924 | "originalRequest": { 925 | "method": "GET", 926 | "header": [ 927 | { 928 | "key": "Accept", 929 | "value": "application/json" 930 | } 931 | ], 932 | "url": { 933 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id", 934 | "host": [ 935 | "{{baseUrl}}" 936 | ], 937 | "path": [ 938 | "sheets", 939 | ":spreadsheet_id" 940 | ], 941 | "variable": [ 942 | { 943 | "key": "spreadsheet_id" 944 | } 945 | ] 946 | } 947 | }, 948 | "status": "OK", 949 | "code": 200, 950 | "_postman_previewlanguage": "json", 951 | "header": [ 952 | { 953 | "key": "Content-Type", 954 | "value": "application/json" 955 | } 956 | ], 957 | "cookie": [], 958 | "body": "{\n \"data\": [\n {\n \"title\": \"\",\n \"index\": \"\",\n \"sheetId\": \"\",\n \"rowCount\": \"\",\n \"columnCount\": \"\"\n },\n {\n \"title\": \"\",\n \"index\": \"\",\n \"sheetId\": \"\",\n \"rowCount\": \"\",\n \"columnCount\": \"\"\n }\n ]\n}" 959 | }, 960 | { 961 | "name": "Empty data", 962 | "originalRequest": { 963 | "method": "GET", 964 | "header": [ 965 | { 966 | "key": "Accept", 967 | "value": "application/json" 968 | } 969 | ], 970 | "url": { 971 | "raw": "{{baseUrl}}/sheets/:spreadsheet_id", 972 | "host": [ 973 | "{{baseUrl}}" 974 | ], 975 | "path": [ 976 | "sheets", 977 | ":spreadsheet_id" 978 | ], 979 | "variable": [ 980 | { 981 | "key": "spreadsheet_id" 982 | } 983 | ] 984 | } 985 | }, 986 | "status": "Bad Request", 987 | "code": 400, 988 | "_postman_previewlanguage": "json", 989 | "header": [ 990 | { 991 | "key": "Content-Type", 992 | "value": "application/json" 993 | } 994 | ], 995 | "cookie": [], 996 | "body": "{\n \"message\": \"\",\n \"data\": \"\"\n}" 997 | } 998 | ] 999 | } 1000 | ] 1001 | }, 1002 | { 1003 | "name": "List all files", 1004 | "request": { 1005 | "method": "GET", 1006 | "header": [ 1007 | { 1008 | "key": "Accept", 1009 | "value": "application/json" 1010 | } 1011 | ], 1012 | "url": { 1013 | "raw": "{{baseUrl}}/sheets", 1014 | "host": [ 1015 | "{{baseUrl}}" 1016 | ], 1017 | "path": [ 1018 | "sheets" 1019 | ] 1020 | }, 1021 | "description": "List down all the spreadsheets can be accessed by the service account." 1022 | }, 1023 | "response": [ 1024 | { 1025 | "name": "Successful operation", 1026 | "originalRequest": { 1027 | "method": "GET", 1028 | "header": [ 1029 | { 1030 | "key": "Accept", 1031 | "value": "application/json" 1032 | } 1033 | ], 1034 | "url": { 1035 | "raw": "{{baseUrl}}/sheets", 1036 | "host": [ 1037 | "{{baseUrl}}" 1038 | ], 1039 | "path": [ 1040 | "sheets" 1041 | ] 1042 | } 1043 | }, 1044 | "status": "OK", 1045 | "code": 200, 1046 | "_postman_previewlanguage": "json", 1047 | "header": [ 1048 | { 1049 | "key": "Content-Type", 1050 | "value": "application/json" 1051 | } 1052 | ], 1053 | "cookie": [], 1054 | "body": "{\n \"data\": [\n {\n \"id\": \"\",\n \"name\": \"\",\n \"modifiedTime\": \"\"\n },\n {\n \"id\": \"\",\n \"name\": \"\",\n \"modifiedTime\": \"\"\n }\n ]\n}" 1055 | }, 1056 | { 1057 | "name": "Empty data", 1058 | "originalRequest": { 1059 | "method": "GET", 1060 | "header": [ 1061 | { 1062 | "key": "Accept", 1063 | "value": "application/json" 1064 | } 1065 | ], 1066 | "url": { 1067 | "raw": "{{baseUrl}}/sheets", 1068 | "host": [ 1069 | "{{baseUrl}}" 1070 | ], 1071 | "path": [ 1072 | "sheets" 1073 | ] 1074 | } 1075 | }, 1076 | "status": "Bad Request", 1077 | "code": 400, 1078 | "_postman_previewlanguage": "json", 1079 | "header": [ 1080 | { 1081 | "key": "Content-Type", 1082 | "value": "application/json" 1083 | } 1084 | ], 1085 | "cookie": [], 1086 | "body": "{\n \"message\": \"\",\n \"data\": \"\"\n}" 1087 | } 1088 | ] 1089 | } 1090 | ] 1091 | } 1092 | ], 1093 | "variable": [ 1094 | { 1095 | "key": "baseUrl", 1096 | "value": "http://localhost:3000/api/v1" 1097 | } 1098 | ] 1099 | } --------------------------------------------------------------------------------