Validate Ports Easily With Zod: A Ready-Made Schema

by Admin 52 views
Validate Ports Easily with Zod: A Ready-Made Schema

Hey guys! Today, we're diving deep into how to validate port numbers using Zod, a fantastic schema declaration and validation library for TypeScript and JavaScript. We're going to create a ready-made schema for validating ports, complete with examples and error handling. Let's get started!

Introduction to Port Validation with Zod

When building applications, especially those involving networking, validating port numbers is crucial. A port number is a 16-bit unsigned integer, meaning it ranges from 0 to 65535. Ports are used to identify specific processes or network services. Using Zod, we can ensure that our application only accepts valid port numbers, preventing potential issues and enhancing security.

Why use Zod, you ask? Well, Zod allows us to define schemas that not only validate data but also provide TypeScript types, giving us end-to-end type safety. This means fewer runtime errors and a more robust application. Plus, Zod’s composable and readable API makes defining complex validation rules a breeze. Let's explore how to create a z.port() schema that handles both string and number inputs, with proper coercion and error messages.

Implementing z.port()

To implement the z.port() schema, we'll extend Zod's existing functionality. This involves creating a custom Zod type that checks if the input is a valid port number. Here’s how we can do it:

Defining the Schema

First, let's define the basic schema. We want it to:

  1. Accept both numbers and strings as input.
  2. Ensure the input can be coerced into a number.
  3. Verify that the number falls within the valid port range (0-65535).

Here’s the code:

import { z } from 'zod';

z.port = () => {
  return z.preprocess(
    (val: any) => {
      if (typeof val === 'string') {
        const parsed = parseInt(val, 10);
        if (!isNaN(parsed)) {
          return parsed;
        }
      }
      return val;
    },
    z.number()
      .min(0, { message: 'Port must be at least 0' })
      .max(65535, { message: 'Port must be at most 65535' })
      .int({ message: 'Port must be an integer' })
  );
};

declare module 'zod' {
  interface ZodType {
    port: () => z.ZodEffects<this, number, number>;
  }
}

Explanation

  • z.preprocess: This is where the magic happens. We use z.preprocess to transform the input before validation. If the input is a string, we try to parse it into an integer. If parsing fails (i.e., isNaN(parsed)), we return the original value.
  • z.number(): We then validate that the (possibly transformed) input is a number.
  • .min(0) and .max(65535): These methods ensure that the number falls within the valid port range.
  • .int(): This verifies that the number is an integer.
  • declare module 'zod': This TypeScript declaration augments the zod module to include our new port method. This allows us to call z.port() directly.

Comprehensive Code Examples

Now, let's look at some examples of how to use this schema.

Validating a Port Number

const portSchema = z.port();

// Valid ports
console.log(portSchema.safeParse(8080)); // Success
console.log(portSchema.safeParse("8080")); // Success

// Invalid ports
console.log(portSchema.safeParse(-1));   // Failure
console.log(portSchema.safeParse(65536)); // Failure
console.log(portSchema.safeParse("abc"));  // Failure

Using .coerce

Zod’s .coerce method is incredibly useful for automatically transforming the input to the desired type. Let’s integrate it into our port validation:

import { z } from 'zod';

z.port = () => {
    return z.coerce.number()
        .min(0, { message: 'Port must be at least 0' })
        .max(65535, { message: 'Port must be at most 65535' })
        .int({ message: 'Port must be an integer' });
};

declare module 'zod' {
    interface ZodType {
        port: () => z.ZodEffects<this, number, number>;
    }
}

const coercedPortSchema = z.port();

// Valid ports
console.log(coercedPortSchema.safeParse(8080));       // Success
console.log(coercedPortSchema.safeParse("8080"));     // Success
console.log(coercedPortSchema.safeParse(8080.5));   // Failure, not an integer

// Invalid ports
console.log(coercedPortSchema.safeParse(-1));         // Failure
console.log(coercedPortSchema.safeParse(65536));       // Failure
console.log(coercedPortSchema.safeParse("abc"));       // Failure, cannot coerce

TypeScript Typings

To ensure full TypeScript support, we need to augment the Zod module. This tells TypeScript that z.port() exists and what type it returns.

import { z } from 'zod';

declare module 'zod' {
  interface ZodType {
    port: () => z.ZodEffects<this, number, number>;
  }
}

This snippet extends the ZodType interface to include our port method. The return type z.ZodEffects<this, number, number> indicates that the method returns a Zod schema that transforms the input to a number.

Error Handling

Effective error handling is vital for providing useful feedback to users. Zod makes this easy with its .safeParse() method and customizable error messages.

Using .safeParse()

The .safeParse() method returns an object with either a success property set to true and a data property containing the validated data, or a success property set to false and an error property containing a ZodError object. This allows you to handle validation failures gracefully.

const result = portSchema.safeParse("invalid");

if (!result.success) {
  console.error(result.error.errors);
}

Custom Error Messages

We can customize error messages to provide more context-specific feedback. In our z.port() implementation, we’ve already added custom messages for the .min(), .max(), and .int() methods.

z.number()
  .min(0, { message: 'Port must be at least 0' })
  .max(65535, { message: 'Port must be at most 65535' })
  .int({ message: 'Port must be an integer' })

These messages will be included in the ZodError object when validation fails, making it easier to debug and provide user-friendly error messages.

Common Uses and Best Practices

Let’s explore some common use cases and best practices for validating ports with Zod.

Configuration Files

Validating ports is particularly useful when reading configuration files. You can ensure that the port numbers specified in the configuration are valid before starting your application.

import { z } from 'zod';
import fs from 'fs';
import path from 'path';

z.port = () => {
    return z.preprocess(
        (val: any) => {
            if (typeof val === 'string') {
                const parsed = parseInt(val, 10);
                if (!isNaN(parsed)) {
                    return parsed;
                }
            }
            return val;
        },
        z.number()
            .min(0, { message: 'Port must be at least 0' })
            .max(65535, { message: 'Port must be at most 65535' })
            .int({ message: 'Port must be an integer' })
    );
};

declare module 'zod' {
    interface ZodType {
        port: () => z.ZodEffects<this, number, number>;
    }
}

const configSchema = z.object({
  port: z.port(),
  host: z.string(),
});

try {
  const configFile = fs.readFileSync(path.join(__dirname, 'config.json'), 'utf-8');
  const config = JSON.parse(configFile);
  const validatedConfig = configSchema.parse(config);
  console.log('Validated config:', validatedConfig);
} catch (error) {
  console.error('Error validating config:', error);
}

Environment Variables

Similarly, when reading environment variables, it’s important to validate that the port numbers are correct.

import { z } from 'zod';

z.port = () => {
    return z.preprocess(
        (val: any) => {
            if (typeof val === 'string') {
                const parsed = parseInt(val, 10);
                if (!isNaN(parsed)) {
                    return parsed;
                }
            }
            return val;
        },
        z.number()
            .min(0, { message: 'Port must be at least 0' })
            .max(65535, { message: 'Port must be at most 65535' })
            .int({ message: 'Port must be an integer' })
    );
};

declare module 'zod' {
    interface ZodType {
        port: () => z.ZodEffects<this, number, number>;
    }
}

const envSchema = z.object({
  PORT: z.port().default(3000),
  NODE_ENV: z.enum(['development', 'production']).default('development'),
});

const env = envSchema.safeParse(process.env);

if (!env.success) {
  console.error('Invalid environment variables:', env.error.format());
  process.exit(1);
}

console.log('Environment variables:', env.data);

Input Validation in APIs

When building APIs, you should always validate the input data. This includes validating port numbers.

import { z } from 'zod';
import express from 'express';

z.port = () => {
    return z.preprocess(
        (val: any) => {
            if (typeof val === 'string') {
                const parsed = parseInt(val, 10);
                if (!isNaN(parsed)) {
                    return parsed;
                }
            }
            return val;
        },
        z.number()
            .min(0, { message: 'Port must be at least 0' })
            .max(65535, { message: 'Port must be at most 65535' })
            .int({ message: 'Port must be an integer' })
    );
};

declare module 'zod' {
    interface ZodType {
        port: () => z.ZodEffects<this, number, number>;
    }
}

const app = express();
app.use(express.json());

const apiSchema = z.object({
  port: z.port(),
});

app.post('/api/config', (req, res) => {
  const result = apiSchema.safeParse(req.body);

  if (!result.success) {
    return res.status(400).json({ errors: result.error.errors });
  }

  console.log('Received valid config:', result.data);
  return res.json({ message: 'Config updated successfully' });
});

app.listen(3000, () => {
  console.log('Server listening on port 3000');
});

Conclusion

Alright, guys, that’s a wrap! We’ve covered how to create a z.port() schema using Zod for validating port numbers. We’ve explored various use cases, including validating configuration files, environment variables, and API inputs. By using Zod, you can ensure that your application handles port numbers correctly and safely.

Remember to always validate your data, provide clear error messages, and keep your code clean and maintainable. Happy coding!