Skip to main content

Mastering Google Apps Script: A Professional TypeScript Workflow

Building a robust backend on Google Apps Script requires moving beyond the built-in browser editor. For complex projects like the infrastructure powering Antinna, we need a modern development environment that enforces Clean Architecture and DRY principles.

In this comprehensive guide, we will set up a professional workflow using TypeScript for type safety, esbuild for lightning-fast bundling, and clasp for automated deployment.

1. The Directory Structure

A clean architecture begins with a clean file system. We strictly separate our source code (src) from the compiled output (dist). Note that the appsscript.json manifest must live in the dist folder.

Project Directory Tree
Text
antinna-backend/
├── dist/                 # Compiled output (pushed to Apps Script)
│   ├── appsscript.json   # Mandatory Google manifest file
│   └── ...js files
├── src/                  # TypeScript source files
│   ├── auth/
│   │   └── firebase.ts
│   └── index.ts
├── build.mjs             # Esbuild bundling logic
├── cleanup.js            # Directory management script
├── .clasp.json           # Clasp deployment configuration
├── package.json          # Project metadata and dependencies
└── tsconfig.json         # TypeScript configuration

2. Project Metadata & Dependencies (package.json)

Initialize your project environment from scratch using npm init. This setup maps out your development scripts, handles compilation targets, and organizes external packages efficiently.

This structural manifest allows you to effortlessly plug in your own custom dependencies, ensuring absolute consistency across your entire workspace topology. For example, you can include custom schema models like "@antinna/schema-ld-types": "^1.0.0" to align your backend payloads under a strict type-safe environment.

package.json
JSON
{
  "name": "backend",
  "type": "module",
  "version": "1.0.0",
  "description": "Put Description of Project Here ",
  "keywords": [
    "appscript",
    "backend"
   
  ],
  "homepage": "https://github.com/user_name/repo_name#readme",
  "bugs": {
    "url": "https://github.com/user_name/repo_name/issues"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/user_name/repo_name.git"
  },
  "license": "MIT",
  "author": "Manishmg3994",
  "scripts": {
    "cleanup": "node cleanup.js",
    "typecheck": "tsc --noEmit",
    "build:dev": "npm run cleanup && npm run typecheck && node build.mjs",
    "build:prod": "npm run cleanup && npm run typecheck && node build.mjs --prod",
    "dev": "npm run cleanup && npm run typecheck && node build.mjs --watch",
    "push": "clasp push"
  },
  "devDependencies": {
    "@google/clasp": "^3.3.0",
    "@types/google-apps-script": "^1.0.83",
    "esbuild": "^0.28.1",
    "glob": "^13.0.6",
    "typescript": "^7.0.0"
  },
  "dependencies": {
    
  }
}

3. TypeScript Configuration (tsconfig.json)

Configure TypeScript to output modern ESNext code into our dist directory, while skipping library checks to keep the build process fast.

tsconfig.json
JSON
{
  "compilerOptions": {
    "target": "ESNext",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "types": ["google-apps-script"]
  },
  "include": ["src/**/*.ts"]
}

4. Pre-build Cleanup (cleanup.js)

Before every build, we want to wipe the dist directory to prevent stale files from being pushed to Google. However, we must absolutely preserve the appsscript.json file.

cleanup.js
JavaScript
import fs from "node:fs/promises";
import path from "node:path";

const distPath = path.resolve("dist");
const keepFile = "appsscript.json";

try {
    await fs.mkdir(distPath, { recursive: true });
    const entries = await fs.readdir(distPath, { withFileTypes: true });

    for (const entry of entries) {
        if (entry.name === keepFile) {
            continue;
        }

        const entryPath = path.join(distPath, entry.name);

        await fs.rm(entryPath, {
            recursive: true,
            force: true,
        });
    }

    console.log("Cleaned dist directory (except appsscript.json)");
} catch (error) {
    console.error("Failed to clean dist directory:", error);
    process.exit(1);
}

5. The Esbuild Engine (build.mjs)

Google Apps Script expects a flat file structure. This script takes our deeply nested src files and flattens them into the dist folder. For example, auth/firebase.ts automatically becomes auth_firebase.js.

build.mjs
JavaScript
import { build, context } from "esbuild";
import { glob } from "glob";
import path from "node:path";
import fs from "node:fs/promises";

const isWatch = process.argv.includes("--watch");
const isProd = process.argv.includes("--prod");

const files = await glob("src/**/*.ts");

function getOutfile(entry) {
    const relative = path.relative("src", entry);

    // auth/firebase.ts -> auth_firebase.js
    // maps/components/map.ts -> maps_components_map.js
    const fileName = relative.replace(/[\\/]+/g, "_").replace(/\.ts$/, ".js");

    return path.join("dist", fileName);
}

const buildOptions = (entry) => ({
    entryPoints: [entry],
    bundle: true,
    format: "iife",
    platform: "browser",
    target: "esnext",
    minify: isProd,
    outfile: getOutfile(entry),
    logLevel: "info",
});

await fs.mkdir("dist", { recursive: true });

if (isWatch) {
    const contexts = [];

    for (const file of files) {
        const ctx = await context(buildOptions(file));
        await ctx.watch();
        contexts.push(ctx);
    }

    console.log(`Watching ${contexts.length} files...`);
} else {
    await Promise.all(files.map((file) => build(buildOptions(file))));
    console.log(`Built ${files.length} files.`);
}

6. The Clasp Bridge (.clasp.json)

This is where we link our local repository to the remote Google Apps Script project. The critical change here is setting the rootDir to ./dist so clasp ignores our raw TypeScript files. Ensure you replace the scriptId value with your own project's ID.

.clasp.json
JSON
{
  "scriptId": "PUT_YOUR_OWN_SCRIPT_ID_HERE",
  "rootDir": "./dist",
  "scriptExtensions": [".js", ".gs"],
  "htmlExtensions": [".html"],
  "jsonExtensions": [".json"],
  "filePushOrder": [],
  "skipSubdirectories": false
}

7. The Project Manifest (appsscript.json)

The manifest file provides essential operational settings for the Google runtime. Placing this explicitly inside your dist folder ensures that parameters like modern V8 runtime targeting and structured error log forwarding remain completely intact during execution cycles.

dist/appsscript.json
JSON
{
  "timeZone": "America/New_York",
  "dependencies": {
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}

8. Execution Commands

With the system architecture mapped out, execute the following commands in your terminal shell context to build and sync your pipeline:

Workflow Pipeline: Maintain this specific order of terminal operations when setting up or resetting your backend deployment target environment.
  • Initialize Directory Layout: Generate a fresh package schema config file directly within your root structure:
    npm init -y
  • Install Development Engines: Download the tooling bundles required to handle code compilation and Google platform linking:
    npm install -D @google/clasp @types/google-apps-script esbuild glob typescript
  • Inject Custom Modules: Add your custom internal type definitions and structural packages straight to the runtime graph. For example:
    npm install @antinna/schema-ld-types
  • Setup Manifest: Manually copy or create your appsscript.json inside the dist folder. Clasp requires this file to exist before it will push components.
  • Start Development: Run npm run dev. This triggers the cleanup scripts, runs automated type-checking, and watches your codebase targets for real-time compilation.
  • Deploy: When you are ready to update the live Google Apps Script container engine, run npm run push.

Comments

Popular posts from this blog

Flutter™ Installation Without Android Studio™

When developing with Flutter™, you typically install Android Studio™ as part of the setup process. However, if you want a lightweight alternative, you can install Flutter™ with only the Android command-line tools. This guide walks you through the steps to set up Flutter™ on Windows without Android Studio™. Step 1: Install Android Command-Line Tools Download Command-Line Tools: Download the Android SDK Command-Line Tools from the Android Developer website . Extract the downloaded zip file: Extract it to a directory, e.g., C:\Users\<User Name>\AppData\Local\Android\Sdk\cmdline-tools . Set up SDK directories: Inside the cmdline-tools folder, create a subfolder named latest . Move the extracted files and folders into this latest folder. The structure should look like: ...

Building a Modular Project with a Monorepo Using Pub Workspaces Feature

Flutter™  projects often start small, but as they scale, managing multiple packages becomes challenging. If you find yourself working with several modules or features in one project, organizing them efficiently is crucial. Enter Monorepos with Pub Workspaces – an excellent solution for managing a Flutter project with multiple packages in a modular way. In this post, we’ll explore how to structure a Flutter project in a modular way using a monorepo , and how to manage dependencies across various sub-projects without worrying about version conflicts. We’ll also look at how to configure Pub Workspaces to simplify dependency resolution and management. When working with Flutter™ and Dart™ , structuring a project in a modular way can improve scalability and maintainability. This is especially important when managing multiple packages in a single repository, also known as a monorepo . Pub Workspaces , introduced to simplify dependency management, provide a robust solution...

Stop Guessing: A Scalable Strategy for Flutter Environment Management

When building production-grade applications, hardcoding API endpoints, feature flags, or tracking keys directly into your source code is an architectural anti-pattern. To guarantee security and runtime predictability, setups should cleanly isolate target environments depending on compile-time parameters. This guide establishes a compile-time, zero-overhead environment orchestration system leveraging strict encapsulation patterns and modern features in native Dart. 1. Define the Immutable Flavor Domain ( flavor_enum.dart ) The foundation of this pattern relies on an explicit Flavor enumeration. This handles your application's environmental constraints and leverages a resilient static factory fallback to transform incoming build-time string tokens safely into type-safe domains. lib/flavor/flavor_enum.dart Dart /// flavor_enum.dart library; enum Flavor { development, ...