When you have multiple Zuplo projects that share common functionality like
custom policies, handlers, or utility functions, you can create a shared npm
package to avoid duplicating code. This guide shows how to create a reusable
TypeScript module package and automatically copy the source files into your
Zuplo projects.
Overview
The approach is straightforward:
Create an npm package containing your shared TypeScript code and a
postinstall script that copies files to the consumer's modules folder
Publish it to npm or a private registry (or reference it directly via Git)
Install the package in your Zuplo projects - the postinstall script
automatically copies the .ts files into ./modules
Since Zuplo compiles TypeScript at deployment time, you ship raw TypeScript
source files rather than pre-compiled JavaScript. This ensures your shared code
integrates seamlessly with Zuplo's build process.
Creating the Shared Package
Project Structure
Create a new npm package with the following structure:
The files array includes both the source files and the copy script
The postinstall script runs automatically when the package is installed
Use peerDependencies for @zuplo/runtime since consumers provide this
No build step is needed because you're shipping raw TypeScript
Copy Script
Create a script in your shared package that copies files to the consumer's
modules folder. The script adds a header comment to each file indicating it
was auto-generated and should not be edited directly:
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync,} from "fs";import { dirname, join, resolve } from "path";import { fileURLToPath } from "url";const __dirname = dirname(fileURLToPath(import.meta.url));// Source: the src directory in this packageconst sourceDir = resolve(__dirname, "..", "src");// Destination: the modules/shared folder in the consuming project// Navigate up from node_modules/@your-org/shared-zuplo-modules/scriptsconst projectRoot = resolve(__dirname, "..", "..", "..", "..");const destDir = join(projectRoot, "modules", "shared");// Read package.json to get package name and versionconst packageJson = JSON.parse( readFileSync(resolve(__dirname, "..", "package.json"), "utf-8"),);const packageInfo = `${packageJson.name}@${packageJson.version}`;// Header comment to add to copied filesconst header = `/** * AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY * * This file was copied from ${packageInfo} * Any changes made here will be overwritten when the package is updated. * * To modify this code, edit the source in the shared package and republish. */`;// Recursively process and copy filesfunction copyWithHeader(src, dest) { if (!existsSync(dest)) { mkdirSync(dest, { recursive: true }); } const entries = readdirSync(src); for (const entry of entries) { const srcPath = join(src, entry); const destPath = join(dest, entry); if (statSync(srcPath).isDirectory()) { copyWithHeader(srcPath, destPath); } else if (entry.endsWith(".ts")) { // Add header to TypeScript files const content = readFileSync(srcPath, "utf-8"); writeFileSync(destPath, header + content); } else { // Copy other files as-is cpSync(srcPath, destPath); } }}// Copy all files with headersif (existsSync(sourceDir)) { copyWithHeader(sourceDir, destDir); console.log(`✅ Copied shared modules to ${destDir}`);} else { console.warn(`⚠️ Source directory not found: ${sourceDir}`);}
This script runs automatically when someone installs your package, copying your
TypeScript source files directly into their Zuplo project's modules/shared
folder with a header comment indicating the source.
Example Shared Code
Create your shared modules using standard Zuplo patterns:
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";export interface CustomAuthOptions { headerName: string; allowedValues: string[];}export default async function customAuthPolicy( request: ZuploRequest, context: ZuploContext, options: CustomAuthOptions, policyName: string,): Promise<ZuploRequest | Response> { const headerValue = request.headers.get(options.headerName); if (!headerValue) { return new Response(`Missing ${options.headerName} header`, { status: 401, }); } if (!options.allowedValues.includes(headerValue)) { return new Response("Unauthorized", { status: 403 }); } return request;}
my-shared-zuplo-modules/src/utils/helpers.ts
import { ZuploContext } from "@zuplo/runtime";export function formatRequestId(context: ZuploContext): string { return `req-${context.requestId.slice(0, 8)}`;}export function parseJsonSafely<T>(text: string): T | null { try { return JSON.parse(text) as T; } catch { return null; }}
Publishing the Package
Publish to npm or your private registry:
Code
# Public npmnpm publish --access public# Private npm registrynpm publish --registry https://your-registry.example.com# Or use npm link for local developmentnpm link
Alternatively, you can reference the package directly from a Git repository
without publishing:
In your Zuplo project, install the shared package:
Code
npm install @your-org/shared-zuplo-modules
The package's postinstall script automatically copies the TypeScript files to
your modules/shared folder. After installation, your project structure looks
like this:
Use a lockfile (package-lock.json or pnpm-lock.yaml) and commit it to ensure
all team members and CI/CD pipelines use the same version.
Source Control
Commit the Copied Files
The copied shared modules must be committed to your Git repository. Zuplo
deployments require all source files to be present in the repository - they are
not generated during the build process.
After installing or updating the shared package, commit the changes:
The header comments added by the copy script help identify these files as
generated code that should not be edited directly. If you need to make changes,
update the source in the shared package and republish.
Updating Shared Modules
When you update the shared package version, the postinstall script overwrites
the existing files with the new version. Review the changes before committing: