Zod Schema Validation

Demonstrates how to use Zod schemas for argument validation and transformation. Schemas are defined in separate files, making them reusable across commands and testable in isolation.

Deployment Schema

Define a schema for deployment configuration. The schema validates input and transforms it to add computed properties like requiresApproval based on the environment.

1import { z } from 'zod';
2
3/**
4 * Valid deployment environments.
5 */
6export const environments = ['development', 'staging', 'production'] as const;
7
8/**
9 * Deployment configuration schema.
10 * Validates and transforms deployment options.
11 */
12export const deployConfigSchema = z
13  .object({
14    env: z.enum(environments),
15    replicas: z.number().min(1).max(10),
16    dryRun: z.boolean().optional(),
17  })
18  .transform((config) => ({
19    ...config,
20    // Add computed properties based on environment
21    requiresApproval: config.env === 'production',
22    defaultTimeout: config.env === 'production' ? 300 : 60,
23  }));
24
25/**
26 * Type inferred from the schema output (after transform).
27 */
28export type DeployConfig = z.output<typeof deployConfigSchema>;

User Schema

The user schema validates email format and normalizes it to lowercase. It also computes permission flags based on the user's role.

1import { z } from 'zod';
2
3/**
4 * User roles with their permission levels.
5 */
6export const roles = ['viewer', 'editor', 'admin'] as const;
7export type Role = (typeof roles)[number];
8
9/**
10 * User data schema with validation and normalization.
11 */
12export const userSchema = z
13  .object({
14    email: z.string().email('Invalid email format'),
15    role: z.enum(roles),
16    name: z.string().optional(),
17  })
18  .transform((user) => ({
19    ...user,
20    // Normalize email to lowercase
21    email: user.email.toLowerCase(),
22    // Compute permission flags based on role
23    canEdit: user.role === 'editor' || user.role === 'admin',
24    canAdmin: user.role === 'admin',
25  }));
26
27/**
28 * Type inferred from the schema output.
29 */
30export type User = z.output<typeof userSchema>;

CLI with Zod Middleware

The main CLI applies zodMiddleware to each command. This validates parsed arguments against the schema and adds transformed properties to the args object.

1import { cli } from 'cli-forge';
2import { zodMiddleware } from 'cli-forge/middleware/zod';
3
4import { deployConfigSchema, environments } from './schemas/config';
5import { userSchema, roles } from './schemas/user';
6
7const app = cli('validated-cli')
8  .demandCommand()
9
10  .command('deploy', {
11    description: 'Deploy the application',
12    builder: (cmd) =>
13      cmd
14        .option('env', {
15          type: 'string',
16          description: 'Deployment environment',
17          choices: environments,
18          required: true,
19        })
20        .option('replicas', {
21          type: 'number',
22          description: 'Number of replicas',
23          default: 1,
24        })
25        .option('dryRun', {
26          type: 'boolean',
27          description: 'Preview deployment without executing',
28        })
29        .middleware(zodMiddleware(deployConfigSchema)),
30
31    handler: (args) => {
32      // TypeScript knows about transformed properties
33      console.log(`Deploying to ${args.env}...`);
34      console.log(`  replicas: ${args.replicas}`);
35      console.log(`  requiresApproval: ${args.requiresApproval}`);
36      console.log(`  timeout: ${args.defaultTimeout}s`);
37
38      if (args.dryRun) {
39        console.log('Dry run complete.');
40      }
41    },
42  })
43
44  .command('user-info', {
45    description: 'Display user information',
46    builder: (cmd) =>
47      cmd
48        .option('email', {
49          type: 'string',
50          description: 'User email address',
51          required: true,
52        })
53        .option('role', {
54          type: 'string',
55          description: 'User role',
56          choices: roles,
57          required: true,
58        })
59        .option('name', {
60          type: 'string',
61          description: 'Display name',
62        })
63        .middleware(zodMiddleware(userSchema)),
64
65    handler: (args) => {
66      // Email is normalized, permission flags are computed
67      console.log(`User Information:`);
68      console.log(`  Email: ${args.email}`);
69      console.log(`  Role: ${args.role}`);
70      console.log(`  Can Edit: ${args.canEdit}`);
71      console.log(`  Can Admin: ${args.canAdmin}`);
72
73      if (args.name) {
74        console.log(`  Name: ${args.name}`);
75      }
76    },
77  });
78
79export default app;
80
81if (require.main === module) {
82  app.forge();
83}

How It Works

  1. CLI options are defined with basic types (string, number, boolean)
  2. zodMiddleware wraps a Zod schema and returns a middleware function
  3. When the command runs, the middleware validates args against the schema
  4. If validation fails, Zod's error messages are displayed
  5. If validation passes, the transformed output (including computed properties) becomes the new args

Benefits

  • Validation: Complex validation rules (email format, number ranges) with clear error messages
  • Transformation: Normalize data and compute derived values
  • Type Safety: TypeScript infers the output type from the schema, including transformed properties
  • Reusability: Schemas can be shared across commands or used in other parts of your application

All Example Files

FILE EXPLORER
cli.ts
1import { cli } from 'cli-forge';
2import { zodMiddleware } from 'cli-forge/middleware/zod';
3
4import { deployConfigSchema, environments } from './schemas/config';
5import { userSchema, roles } from './schemas/user';
6
7const app = cli('validated-cli')
8  .demandCommand()
9
10  .command('deploy', {
11    description: 'Deploy the application',
12    builder: (cmd) =>
13      cmd
14        .option('env', {
15          type: 'string',
16          description: 'Deployment environment',
17          choices: environments,
18          required: true,
19        })
20        .option('replicas', {
21          type: 'number',
22          description: 'Number of replicas',
23          default: 1,
24        })
25        .option('dryRun', {
26          type: 'boolean',
27          description: 'Preview deployment without executing',
28        })
29        .middleware(zodMiddleware(deployConfigSchema)),
30
31    handler: (args) => {
32      // TypeScript knows about transformed properties
33      console.log(`Deploying to ${args.env}...`);
34      console.log(`  replicas: ${args.replicas}`);
35      console.log(`  requiresApproval: ${args.requiresApproval}`);
36      console.log(`  timeout: ${args.defaultTimeout}s`);
37
38      if (args.dryRun) {
39        console.log('Dry run complete.');
40      }
41    },
42  })
43
44  .command('user-info', {
45    description: 'Display user information',
46    builder: (cmd) =>
47      cmd
48        .option('email', {
49          type: 'string',
50          description: 'User email address',
51          required: true,
52        })
53        .option('role', {
54          type: 'string',
55          description: 'User role',
56          choices: roles,
57          required: true,
58        })
59        .option('name', {
60          type: 'string',
61          description: 'Display name',
62        })
63        .middleware(zodMiddleware(userSchema)),
64
65    handler: (args) => {
66      // Email is normalized, permission flags are computed
67      console.log(`User Information:`);
68      console.log(`  Email: ${args.email}`);
69      console.log(`  Role: ${args.role}`);
70      console.log(`  Can Edit: ${args.canEdit}`);
71      console.log(`  Can Admin: ${args.canAdmin}`);
72
73      if (args.name) {
74        console.log(`  Name: ${args.name}`);
75      }
76    },
77  });
78
79export default app;
80
81if (require.main === module) {
82  app.forge();
83}
84