Chapter 29: Delivery and Maintenance
Building software is one thing; getting it into the world and keeping it healthy is another. Delivery shapes how users experience your tools, while maintenance determines how peacefully your team sleeps at night. With TypeScript you can describe deployment inputs, model release behaviour, and guide safe upgrades with helpful types and scripts.
Distribution formats and build artifacts
Node applications travel in several shapes. Each shape balances portability, size, and runtime traits. TypeScript’s build step produces JavaScript, declaration files, and source maps. From there you can bundle your code, pack it into archives, or publish to registries. The right choice depends on how the software will be used and how fast it must start.
Plain Node packages
A plain package includes compiled JavaScript, .d.ts files, and metadata. This works well for libraries and services that run in environments with Node. Keep your output directory clean and avoid shipping tests or raw TypeScript unless users expect them.
// tsconfig.build.json
{
"compilerOptions": {
"outDir": "dist",
"declaration": true,
"sourceMap": true,
"module": "esnext"
},
"include": ["src"]
}
Bundled artifacts
Bundling with tools such as esbuild or Rollup produces a single file that starts quickly. This suits CLI utilities, lightweight services, and tasks that run in short lived environments. Minification trims size, although you must keep source maps when debugging matters.
// esbuild.mjs
import { build } from 'esbuild';
await build({
entryPoints: ['src/cli.ts'],
outfile: 'dist/cli.js',
bundle: true,
platform: 'node',
sourcemap: true
});
Containers and serverless packages
Containers wrap your program with its runtime. Keep images small and deterministic. Serverless targets often require compact bundles and explicit handler exports. In both cases, ensure dependencies are pinned and builds are reproducible.
# Dockerfile
FROM node:20-slim
WORKDIR /app
COPY dist/ .
CMD ["node", "server.js"]
Runtime configuration typing
Every deployed system needs configuration. Typed configuration reduces surprises and helps catch mistakes before they leak into production. Represent the shape of environment variables and external settings with schemas, then validate at startup.
Typed configuration objects
Keep configuration near the entry point of your service. Use a schema so you can read from process.env, parse values, and supply defaults. Invalid configuration should stop the program early.
// config.ts
import { z } from 'zod';
const ConfigSchema = z.object({
port: z.number().int().positive(),
dbUrl: z.string().url(),
featureFlag: z.boolean().default(false)
});
export type Config = z.infer<typeof ConfigSchema>;
export function load(): Config {
const raw = {
port: Number(process.env.PORT),
dbUrl: process.env.DB_URL,
featureFlag: process.env.FEATURE_FLAG === 'true'
};
return ConfigSchema.parse(raw);
}
Separating secrets safely
Configuration and secrets belong together logically but should be retrieved from different sources. Load secrets from encrypted stores or environment variables. Keep them typed so misuse is easy to detect during reviews.
Runtime feature flags
Feature flags let you ship code early and enable features gradually. Model flags with a small interface. Keep flag checks simple so behaviour stays readable.
// flags.ts
export interface FeatureFlags {
newCheckout: boolean;
useFastPath: boolean;
}
export function enabled(f: FeatureFlags, name: keyof FeatureFlags) {
return f[name];
}
Automated releases and changelogs
Releases document how your project evolves. Automated pipelines reduce manual work and keep tags, versions, and notes in sync. Changelog generation encourages good commit messages and shows users what changed in each version.
Semantic versioning with automation
Semantic release tools analyse commit messages, decide the next version, update the changelog, publish packages, and tag the repository. This keeps releases predictable and keeps human effort on content rather than mechanics.
// .releaserc
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
"@semantic-release/github"
]
}
Keeping changelogs readable
A clear changelog groups changes into sections such as added, fixed, changed, and removed. Automated tools can generate this, but you can refine the language when publishing a major release. Readers appreciate clarity over volume.
// excerpt from CHANGELOG.md
## 1.4.0
- Added new schema validator for configs
- Fixed race condition in job scheduler
- Improved startup time by bundling CLI
Release candidates and verification
When large changes arrive, publish release candidates with a suffix such as -rc.1. This pattern encourages early testing without surprising production systems.
Deprecations and upgrading safely
All software grows and sheds old behaviour. Deprecation policies guide your users toward safer patterns. This requires clear communication, migration paths, and tooling that detects old calls at compile time when possible.
Marking deprecated APIs
TypeScript supports the @deprecated tag in JSDoc. Editors flag these APIs with warnings. Deprecate when you know a better alternative exists and when the old API will be removed in a predictable future release.
// example.ts
/**
* @deprecated Use calculateTotal instead
*/
export function legacyTotal(x: number, y: number) {
return x + y;
}
Migration helpers
Provide codemods or small scripts that rewrite old imports and calls. This reduces friction when upgrading and helps teams avoid manual hunting for outdated patterns.
// codemod sketch
// find legacyTotal and replace with calculateTotal
export function rewrite(source: string) {
return source.replace(/legacyTotal/g, 'calculateTotal');
}
Safe removal windows
When removing a deprecated feature, define a window where both behaviours coexist. Communicate the timeline in changelogs and documentation. Do not surprise downstream consumers with silent removals.
Tracking upgrade pain points
Major version upgrades reveal rough edges. Keep a list of common breakages, note where types changed unexpectedly, and publish a migration guide. Continuous improvements make future upgrades easier.
With strong distribution habits, typed configuration, automated releases, and thoughtful deprecations, your TypeScript and Node projects can evolve smoothly and stay dependable long after the first deployment.