How to Build a Typescript NPM Package For a Cloudflare Worker

Learn how to architect your NPM package for fast development, small size, and simple deployment to the package repository.

TypeScriptNPMCloudflare Workers

In this tutorial, you will learn:

  1. How to architect your Cloudflare Worker-focused NPM package.
  2. How to publish your package on NPM.

Once you’re done, you’ll have everything you need to release Cloudflare Worker packages fast!

Quick Start

This tutorial is based on a template I’ve made for my NPM packages. You can find it here:

GitHub - Connor56/cloudflare-worker-npm-package: A skeleton structure for a CloudFlare worker npm…

To start a new package, clone it using SSH with:

bash
git clone git@github.com:Connor56/cloudflare-worker-npm-package.git

Or using HTTP with:

bash
git clone https://github.com/Connor56/cloudflare-worker-npm-package.git

Rename the repo with:

bash
mv cloudflare-worker-npm-package <your-package-name>

Delete the pre-existing .git with:

bash
cd <your-package-name>

You may have to use an elevated terminal command to dete the .git folder:

bash
sudo rm -r .git

Then reinitialise the repo with:

bash
git init

This will give you a fresh start to build from.

If you already understand NPM and TypeScript publishing, that’s all you need! Read on to learn more about the decisions behind the package’s design.

Step 1 — How To Architect Your Package

The Overall Structure

I recommend this package structure:

bash
📁 ./
├── 📁 test/
│   └── (... your tests)
├── 📁 src/
│   └── (... your source code)
├── 📄 .gitignore
├── 📄 LICENCE
├── 📄 README.md
├── 📄 package.json
├── 📄 tsconfig.json

It has everything you need:

  1. The obligatory package.json to define your package.
  2. A dedicated place for your source code: /src
  3. A dedicated place for testing: /test
  4. A copyright licence.
  5. A README.md introducing people to your package.
  6. tsconfig.json for configuring the TypeScript compilation step.
  7. .gitignore for keeping things like node_modules and your compiled package distribution out of the repo.

Most importantly, it’s a simple base that’s easy to build from. A detailed explanation of the most important parts is below.

The package.json

This is the structure as of 2025–12–24:

json
{
  "name": "my-package-name",
  "version": "0.1.0",
  "description": "My package description.",
  "author": "Connor Skelland",
  "license": "MIT",
  "engines": {
    "node": ">=18.0.0"
  },
  "keywords": ["something", "something else"],
  "type": "module",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "test": "vitest run",
    "clean": "tsc --build --clean",
    "build": "tsc --build",
    "build:watch": "tsc --watch",
    "prepublishOnly": "npm run test && npm run clean && npm run build"
  },
  "devDependencies": {
    "@cloudflare/workers-types": "^4.20250816.0",
    "@types/node": "^24.3.0",
    "typescript": "^5.9.2",
    "vitest": "^3.1.0"
  },
  "files": []
}

Here is the purpose behind each of these options:

  • name Your package’s unique identifier on NPM. This is how users will install your package: npm install my-package-name
  • version Current version of your package (follows semver). Start at 0.1.0 and increment with each release. NPM uses this to track updates.
  • description Brief summary of what your package does. Appears in NPM search results and helps users understand your package.
  • author Your name or organization. Shows who maintains the package.
  • license Type of license (e.g., MIT, ISC). Tells users how they can legally use your code. MIT is very permissive.
  • engines Minimum Node.js version required. >=18.0.0 ensures users have a Node version that supports your code.
  • keywords Array of search terms. Helps people discover your package on NPM when searching for related topics.
  • type Set to "module" for ES modules. Critical for Cloudflare Workers — enables modern import/export syntax instead of old require().
  • types Location of TypeScript type definitions. Points to dist/index.d.ts so TypeScript users get autocomplete and type checking.
  • exports Defines package entry points. Modern way to specify what users get when they import your package.
  • exports["."] The default/main entry point. The "." means this is what users get when they do import pkg from 'my-package-name'.
  • exports["."].import Points to your compiled JavaScript. The actual code that runs when someone imports your package.
  • exports["."].types Points to your type definitions. Tells TypeScript where to find type information.
  • scripts Commands you can run with npm run. Automates common tasks like testing and building.
  • scripts.test Runs vitest run. Executes your test suite once. I prefer single execution
  • scripts.clean Runs tsc --build --clean. Removes all compiled files from the dist folder (cleanup before rebuilding).
  • scripts.build Runs tsc --build. Compiles your TypeScript code to JavaScript and generates type definitions using the TypeScript compiler.
  • scripts.build:watch Runs tsc --watch. Continuously watches for changes and rebuilds automatically (useful during development).
  • scripts.prepublishOnly Runs npm run test && npm run clean && npm run build. Lifecycle hook — tests, cleans, and builds your package before publishing to NPM (prevents publishing stale, broken, or unbuilt code).
  • devDependencies Tools needed for development only. These aren’t installed when users add your package to their project.
  • @cloudflare/workers-types TypeScript types for Cloudflare Workers. Provides autocomplete and type checking for Workers-specific APIs.
  • @types/node TypeScript types for Node.js. Provides types for Node.js built-in modules. A large number of these are polyfilled by Cloudflare Workers.
  • typescript The TypeScript compiler. Enables writing type-safe code.
  • vitest Fast testing framework. Modern, fast alternative to Jest with great TypeScript support.
  • files Whitelist of files to publish. Works like a reverse .gitignore — only listed files/folders get published. Empty means everything is included.

If you want more information, you can read the npm documentation:

package.json | npm Docs

The tsconfig.json

This is the tsconfig.json as of 2025–12–25:

json
{
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./dist",
    "module": "nodenext",
    "target": "esnext",
    "types": ["@cloudflare/workers-types"],
    "declaration": true,

    // Opinionated options below
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noPropertyAccessFromIndexSignature": true,
    "strict": true,
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "noUncheckedSideEffectImports": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "test"]
}

The top set of options in compilerOptions are technically all you need. Th remaining compilerOptions are opinionated settings that force you to write solid TypeScript. Delete at your leisure.

Here’s the purpose of all the options:

  • rootDir The root directory of your source code. Set to ./src so TypeScript knows where to find your source files.
  • outDir The output directory for compiled code. Set to ./dist so all compiled JavaScript and type definitions go in one place.
  • module The module code generation mode. nodenext ensures compatibility with modern Node.js ESM (ES Modules). See also TypeScript Module Documentation.
  • target The target ECMAScript version. esnext compiles to the latest JavaScript features, making it perfect for the Cloudflare Workers runtime, which run in V8 engine isolates.
  • types Type definitions to include. @cloudflare/workers-types gives you autocomplete and type checking for Cloudflare Workers APIs.
  • declaration Generates .d.ts declaration files. Set to true so TypeScript creates type definition files that consumers of your package can use.
  • noUncheckedIndexedAccess Enforces strict checking of indexed access. Prevents bugs by making TypeScript warn when accessing array/object properties that might not exist.
  • noUnusedLocals Enforces that all local variables are used. Helps keep code clean by catching unused variables.
  • noUnusedParameters Enforces that all parameters are used. Catches unused function parameters, improving code quality.
  • noPropertyAccessFromIndexSignature Prevents accessing undefined properties. Forces you to use bracket notation for dynamic property access, preventing typos.
  • strict Enforces strict type checking. Enables all strict type checking options — catches more bugs at compile time.
  • verbatimModuleSyntax Forces explicit type imports. Requires you to write import type when only importing types — making bundling more efficient.
  • isolatedModules Enforces strong module boundaries. Each file must be independently compilable to ensure compatibility with bundlers and build tools.
  • noUncheckedSideEffectImports Prevents importing non-existent modules. Catches typos in import statements before runtime.
  • skipLibCheck Skips checking external library types. Speeds up compilation by not type-checking node_modules.
  • forceConsistentCasingInFileNames Enforces consistent casing in file names. Prevents issues when moving code between case-sensitive (Linux) and case-insensitive (Windows/Mac) filesystems.
  • include Files to include in compilation. ["src/**/*"] tells TypeScript to compile everything in the src folder.
  • exclude Files to exclude from compilation. Prevents TypeScript from unnecessarily processing node_modules, dist, and test folders.

If you want more information, you can read the tsconfig reference:

TSConfig Reference - Docs on every TSConfig option

Why the simple structure?

My biggest pet peeve in a project is root directory detritus. Even something as messy as this makes my skin crawl:

lua
📁 ./

Sometimes you need every bit of it. But it’s still an ugly cognitive overload. Starting from a simple template means you own any extra files that end up in your root. With ownership comes understanding and the absolute permission to tidy and optimise without fear.

In short, I’ve kept it simple to keep you nimble.

Package Extensions

If you’d prefer decisions to be made for you, here are some things you may want to add:

  1. A formatter.
  2. Pre-commit hooks.
  3. /docs folder for extra examples.
  4. /scripts folder for custom repo actions.
  5. CI/CD pipeline.
  6. Automated versioning.

I’ve added links to the most recommended tools, where applicable.

Step 2 — Create an Account on NPM

If you don’t have one already, visit the sign up page:

https://www.npmjs.com/signup

Sign up using:

  • username\\ \\— Choose something fairly bland but identifiable. Or, if you already have a profressional/ developer username, use that. For me, that’s connor56.
  • email — Choose a stable email address you have access to and read regularly. If your email provider allows aliases, you could always add +npm to the front to make them easy to sort.

You’ll be asked to verify your email by a One-Time-Passcode. Once complete you’ll see this screen:

Private Email Alias

If you dislike the idea of making your private email address public but still want to use it for communication, use a relay service. For example, ProtonMail has its Proton Pass manager, which lets you set up aliases:

Proton Pass: Free password manager with identity protection | Proton

If you don’t use ProtonMail, you could also use something like SimpleLogin:

SimpleLogin | Open source anonymous email service

Step 3 — Build and Publish Your Package

Once you’ve created your package and you’re ready to publish, you need to log in to your account on the npm CLI. Do this with:

bash
npm login

You’ll be taken to the npm login screen and sent another One-Time Passcode. Fill this in on the web browser, and you’ll grant your npm CLI permission to use your account:

bash
➜  git:(main) ✗ npm login

Before publishing, you may need to bump the package version if you’ve made changes:

bash
npm bump patch
npm bump minor
npm bump major

Then publish with:

bash
npm publish

If you’ve left the prepublishOnly script in your package.json this will run all of your tests, delete all of the code from the /dist folder, and rebuild everything. If there are any issues, publishing will fail.

If you’re being told you need to pay to publish a private package, it’s probably because you have a scope in front of the package name. For example:

bash
@connor56/some-package

When you use a scope the npm CLI assumes you want to publish your package privately. You can prevent this by running:

bash
npm publish --access public

That’s it, you’ll see your package on your NPM account:

Final Words

Well, there you are! I hope this helps you set up and build your own reusable software for Cloudflare Workers. If it’s your first time releasing anything on NPM, let me know if the explanations help you to get over the first publication nerves.

Happy publishing!