# Đọc và ghi Google Sheets, Drive bằng Node.js và service account: từng bước một

> Chỉ cần share bảng tính cho email của một service account, script Node.js của bạn đọc được đơn hàng, ghi báo cáo và đẩy file lên Drive mà không phải chờ đội IT của khách cấp quyền toàn domain.

Bản gốc: https://fdetimes.net/vi/bach-khoa/thuc-hanh-google-workspace-api-apps-script-service-account/

Tuần đầu ở một khách hàng mới, dữ liệu bạn cần hiếm khi nằm trong một database gọn gàng. Thường nó nằm trong một Google Sheet tên kiểu "Đơn hàng – bản cuối – v3", do đội vận hành cập nhật bằng tay mỗi sáng.

Nếu đọc và ghi được bảng tính đó bằng code ngay ngày đầu, bạn đã có pipeline chạy thử trong khi người khác vẫn đang chờ cấp quyền vào kho dữ liệu.

Bên dưới, bạn sẽ dựng từng phần của một script Node.js dùng thư viện chính thức googleapis và một service account. Script đọc một vùng ô, ghi đè, nối thêm dòng, xử lý lỗi hạn mức và chọn đúng kiểu upload lên Drive. Bước nào cũng có một việc cần kiểm tra trước khi sang bước sau.

## Bạn cần chuẩn bị gì?

Bạn cần Node.js, một Google Cloud project mà bạn có quyền tạo service account, và một Google Sheet thử nghiệm. Hãy dùng sheet giả có vài dòng dữ liệu, ví dụ tab `DonHang` gồm các cột mã đơn, số lượng, đơn giá, ngày. Đừng tập trên dữ liệu thật của khách.

Code bên dưới đã được rút gọn cho dễ đọc. Nó dùng `import` và `await` ở cấp cao nhất, nên file cần chạy dưới dạng ES module. Cách khởi tạo client, bạn nên đối chiếu với README của googleapis trước khi chạy.

## Bước 1: tạo service account và cất key cẩn thận

Service account là một "robot account" dùng để xác thực ở cấp ứng dụng, server-to-server. Đó đúng là kiểu xác thực mà một script chạy theo lịch cần. Tạo service account trong project rồi tải file credential JSON về. Từ lúc này, file đó là chìa khóa mở mọi tài liệu được share cho robot.

Tài liệu IAM của Google nói thẳng rằng service account key là một rủi ro bảo mật nếu không được quản lý đúng cách. Người giữ key chịu trách nhiệm về private key và nên ưu tiên các phương án thay thế khi có thể. README của googleapis cũng nhắc phải cất file credential ở chỗ an toàn và không bao giờ đưa nó vào source control.

Cách chắc ăn nhất là để key nằm hẳn ngoài thư mục repo. Khi đó không một lệnh `git add` nào chạm tới được nó:

```bash
mkdir -p ~/.secrets
mv ~/Downloads/key.json ~/.secrets/sa-demo.json
export SA_KEY_PATH=~/.secrets/sa-demo.json
# Thay hai giá trị dưới đây bằng chuỗi scope thật trước khi chạy
export GOOGLE_SCOPES="<scope-sheets> <scope-drive>"
```

Hai giá trị `` và `` chỉ là chỗ giữ chỗ: để nguyên thì script sẽ không chạy. Trước khi chạy, bạn phải tự tra danh sách OAuth scope trong tài liệu của Sheets API và Drive API, chép chính xác chuỗi scope cần dùng, cách nhau bằng dấu cách. Nên chọn scope hẹp nhất đủ cho việc cần làm: script chỉ đọc thì không cần scope ghi.

**Kiểm tra:** chạy `echo $GOOGLE_SCOPES` để chắc không còn dấu `<`, rồi chạy `git status` trong repo và chắc chắn không có file JSON nào chứa key trong danh sách.

## Bước 2: share file cho robot, không cần admin

Mỗi service account có một địa chỉ email duy nhất. Trong phần share của tài liệu, bạn có thể coi email đó như một người dùng bình thường. Mở Google Sheet, bấm Share, dán email của service account, chọn Viewer nếu chỉ đọc hoặc Editor nếu cần ghi.

Ở khách hàng, đây là chi tiết đáng giá nhất của cả kỹ thuật. Bạn không cần đội IT cấp quyền toàn domain. Chủ bảng tính tự share cho robot và cũng tự gỡ được bất cứ lúc nào.

**Kiểm tra:** lấy spreadsheet ID từ URL (đoạn nằm giữa `/d/` và `/edit`) rồi lưu vào biến môi trường `SHEET_ID`.

## Bước 3: đọc một vùng ô

Cài thư viện chính thức từ npm:

```bash
npm install googleapis
```

Để đọc, bạn cần đúng hai thứ: spreadsheet ID và vùng ô viết theo A1 notation. Lời gọi là `spreadsheets.values.get`.

```js
import { google } from 'googleapis';

const SCOPES = process.env.GOOGLE_SCOPES.split(' ');

const auth = new google.auth.GoogleAuth({
  keyFile: process.env.SA_KEY_PATH,
  scopes: SCOPES,
});
const sheets = google.sheets({ version: 'v4', auth });

const res = await sheets.spreadsheets.values.get({
  spreadsheetId: process.env.SHEET_ID,
  range: 'DonHang!A1:D20',
});
console.log(res.data.values);
```

**Kiểm tra:** bạn nhận về một mảng các dòng. Nếu gặp lỗi quyền truy cập, gần như chắc chắn bạn đã bỏ qua Bước 2 hoặc share nhầm email.

## Bước 4: ghi đè hay nối thêm, và Sheets hiểu dữ liệu của bạn ra sao

Muốn ghi, bạn cần ba thứ: spreadsheet ID, vùng A1 và dữ liệu đặt trong request body. Ghi vào một vùng cố định thì dùng `values.update`. Thêm dòng mới vào cuối bảng thì dùng `values.append`.

```js
await sheets.spreadsheets.values.append({
  spreadsheetId: process.env.SHEET_ID,
  range: 'DonHang!A:D',
  valueInputOption: 'USER_ENTERED',
  requestBody: { values: [['DH-1042', 3, 120000, '2026-10-21']] },
});
```

Tham số `valueInputOption` là bắt buộc, và nó quyết định dữ liệu hiện ra thế nào với người dùng. `RAW` chèn chuỗi y nguyên. `USER_ENTERED` phân tích đầu vào đúng như khi có người gõ vào giao diện Sheets, nên công thức và ngày tháng đều được hiểu.

Thử một ví dụ. Ghi chuỗi `=SUM(B2:B4)` bằng `RAW` thì ô hiện nguyên văn dòng chữ đó. Ghi bằng `USER_ENTERED` thì ô thành công thức và hiện tổng. Với `2026-10-21` cũng vậy: một bên là chuỗi, bên kia là ngày mà bộ lọc và biểu đồ của đội vận hành dùng được.

**Điểm mấu chốt:** Với người dùng bảng tính, một ngày bị lưu thành chuỗi chẳng khác gì một bug.

Quy tắc nên nhớ: dữ liệu cho người xem trên Sheets thì dùng `USER_ENTERED`. Còn mã đơn, số điện thoại hay bất cứ thứ gì dễ bị hiểu nhầm thành số hoặc công thức thì ghi bằng `RAW`.

**Kiểm tra:** mở sheet, bấm vào ô ngày và xem nó được định dạng như ngày hay bị căn trái như chữ.

## Bước 5: hạn mức là giới hạn thiết kế, không phải chuyện hiếm gặp

Hạn mức mặc định của Sheets API là 300 request đọc và 300 request ghi mỗi phút cho mỗi project, và 60 mỗi phút cho mỗi người dùng. Vượt quá, bạn nhận lỗi HTTP 429. Google khuyên thử lại bằng thuật toán exponential backoff.

Thử làm một phép tính. Bạn cần đẩy 1.000 dòng vào sheet và gọi `append` riêng cho từng dòng. Như vậy là 1.000 request ghi, gấp hơn ba lần trần 300 mỗi phút của cả project, chưa kể trần 60 mỗi người dùng còn thấp hơn nhiều. Nếu gom cả 1.000 dòng vào mảng `values` của một lần `append`, bạn chỉ còn một request.

Dù đã gom batch, bạn vẫn nên bọc mọi lời gọi bằng một hàm retry:

```js
async function withBackoff(fn, maxTries = 5) {
  for (let i = 0; ; i++) {
    try { return await fn(); }
    catch (e) {
      if (e.code !== 429 || i >= maxTries - 1) throw e;
      const wait = 2 ** i * 1000 + Math.random() * 1000;
      await new Promise(r => setTimeout(r, wait));
    }
  }
}
```

Hàm này đã được đơn giản hóa. Nó chỉ xét mã 429 và chờ lần lượt khoảng 1, 2, 4, 8 giây. Phần ngẫu nhiên cộng thêm giúp nhiều tiến trình không thử lại cùng một lúc.

## Bước 6: chọn kiểu upload lên Drive theo kích thước file

Bước này không đi hết lời gọi upload, mà giúp bạn chọn đúng kiểu upload trước khi viết nó. Drive API có hai kiểu cần biết. File từ 5 MB trở xuống có thể dùng multipart upload, tức là gửi metadata và nội dung trong cùng một request. File lớn hơn 5 MB, hoặc khi mạng dễ đứt giữa chừng, nên dùng resumable upload.

Với resumable upload, mỗi chunk phải là bội số của 256 KB, tức 256 × 1024 = 262.144 byte, trừ chunk cuối cùng. Chẳng hạn, một file báo cáo 20 MB tải theo chunk 8 MB (bằng 32 lần 256 KB) là hợp lệ. Chunk 5.000.000 byte thì không hợp lệ, vì con số đó không chia hết cho 262.144.

```js
const KB256 = 256 * 1024;
function uploadPlan(sizeBytes, chunkBytes = 32 * KB256) {
  if (chunkBytes % KB256 !== 0) throw new Error('chunk phải là bội số 256 KB');
  return sizeBytes <= 5 * 1024 * 1024 ? 'multipart' : 'resumable';
}
```

Hàm trên chỉ là phần lập kế hoạch. Khi viết lời gọi upload thật, hãy lấy đúng cú pháp cho kiểu upload mà hàm trả về từ hướng dẫn upload của Drive API và README của googleapis, rồi bọc nó trong `withBackoff` ở Bước 5.

Thư mục Drive cũng được share cho email của service account như ở Bước 2. Script chỉ ghi được vào nơi nó đã được share.

**Kiểm tra:** gọi `uploadPlan` với một file 3 MB, một file 20 MB và một `chunkBytes` bằng 5.000.000. Bạn phải nhận về lần lượt `multipart`, `resumable` và một lỗi.

## Những lỗi hay gặp nhất

Lỗi phổ biến nhất là commit file key, thường qua một lệnh `git add .` vội vàng. Đó là lý do Bước 1 đặt key hẳn ngoài repo.

Kế đến là share sheet cho email cá nhân của bạn thay vì email của service account, rồi mất nửa giờ đọc lỗi quyền truy cập. Một lỗi khác là ghi bằng `RAW` rồi nghe đội vận hành phàn nàn rằng cột ngày "không lọc được".

Một lỗi khó thấy hơn là vòng lặp gọi API cho từng dòng. Nó chạy êm với 20 dòng thử nghiệm, rồi gãy vì lỗi 429 đúng vào buổi demo có dữ liệu thật.

## Kỹ năng này xuất hiện thế nào ở khách hàng?

Hãy hình dung một khách hàng logistics muốn có agent tóm tắt các đơn hàng trễ mỗi sáng. Phiên bản đầu tiên không cần data warehouse. Agent đọc tab `DonHang`, xử lý, ghi kết quả vào tab `BaoCao` bằng `USER_ENTERED`, rồi xuất file PDF lên một thư mục Drive mà người quản lý đã share sẵn.

Người dùng thấy kết quả ngay trong công cụ họ mở hằng ngày. Đó thường là cách nhanh nhất để có được lòng tin.

Khi đưa kỹ năng này vào CV, đừng ghi chung chung là "biết Google APIs". Hãy viết cụ thể, chẳng hạn: "dựng pipeline Node.js đọc ghi Google Sheets qua service account, gom batch để nằm trong hạn mức API, retry có backoff, key quản lý ngoài repo". Mỗi vế trong câu đó là một câu hỏi phỏng vấn bạn trả lời được.

Script kiểu này nên được coi là bản nháp đầu tiên của hệ thống, chưa phải hệ thống cuối cùng. Nếu nó chứng minh được giá trị, khách hàng sẽ tự hỏi bạn chuyện chuyển lên hạ tầng thật.

**Thử ngay tuần này:**

- Tạo một service account trong project thử nghiệm, share một Google Sheet cho email của nó rồi chạy script đọc vùng A1:D20.
- Ghi cùng chuỗi =SUM(B2:B4) hai lần, một lần bằng RAW và một lần bằng USER_ENTERED, rồi so sánh hai ô kết quả.
- Bọc mọi lời gọi API trong một hàm retry có exponential backoff cho lỗi 429 và đưa hàm đó vào template dự án của bạn.

## Nguồn

- [Basic reading / values guide - Google Sheets API](https://developers.google.com/workspace/sheets/api/guides/values)

- [Usage limits - Google Sheets API](https://developers.google.com/workspace/sheets/api/limits)

- [Create access credentials - Google Workspace](https://developers.google.com/workspace/guides/create-credentials)

- [Service accounts overview - IAM documentation](https://docs.cloud.google.com/iam/docs/service-account-overview)

- [googleapis/google-api-nodejs-client (README)](https://github.com/googleapis/google-api-nodejs-client)

- [Upload file data - Google Drive API](https://developers.google.com/workspace/drive/api/guides/manage-uploads)
