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.
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.
{
"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.
{
"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.
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.
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.
{
"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.
{
"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:
-
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.jsoninside thedistfolder. 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
Post a Comment