DEV Community

Cover image for Reclaim the Fun of Coding: How to Avoid Manual Input Checks
Ben Lue
Ben Lue

Posted on

Reclaim the Fun of Coding: How to Avoid Manual Input Checks

We all know the feeling. You’ve just architected a brilliant feature. The logic is sound, the data flow is elegant, and you’re ready to dive into the "fun part"—building the core functionality that actually does something cool.

But wait. First, you have to build the gatekeeper.

You have to check if userId exists. You have to check if age is actually a number. You have to make sure quantity isn't negative, and that email actually looks like an email. Before you know it, the top 20 lines of your function are a messy soup of if statements, typeof checks, and manual error throwing.

Input verification is critical for security and stability, but it's probably also the most boring part of web development. It could break your flow, clutter your code, and turn a creative process into a tedious chore. Most importantly, it takes the fun out of programming.

Is There A Better Way?

Over the years, I've found there's a better way to do input verification. The suggestion is to move from imperative checking to declarative contracts.

In the traditional imperative approach, we write code that tells the computer how to check the data step-by-step. Below is a typical example:

// The "Chore" Section - Hard to read, hard to maintain
if (!input.username || typeof input.username !== 'string') {
    throw new Error("Username is required");
}
if (input.age && (typeof input.age !== 'number' || input.age < 18)) {
    throw new Error("Must be 18+");
}
if (input.role && !['admin', 'user', 'guest'].includes(input.role)) {
    throw new Error("Invalid role");
}
// Finally... the actual logic starts here
Enter fullscreen mode Exit fullscreen mode

This is quite mechanical and tedious. Even the most patient developers can hardly enjoy reading code like that. What if we could simply declare what valid data looks like and let a utility handle the rest?

Let Inputs Declare Themselves

Instead of writing step-by-step checks, we define a schema — a contract that describes the shape of valid input. We put our effort into a reusable utility that does all the validation for us. Build once, use all the time.

An example can better illustrate this declarative pattern:

{
    username: { 
        '@type': 'string(50)', 
        '@required': true 
    },
    age: { 
        '@type': 'integer[18..]', // Must be integer, 18 or greater
        '@required': false 
    },
    role: { 
        '@type': 'string{admin, user, guest}', // Enum constraint
        '@default': 'user'
    }
}
Enter fullscreen mode Exit fullscreen mode

A descriptor for each input parameter is defined as a JSON object with @type to describe the data type and additional control properties such as @required and @default to further refine the input contracts. The @type property can be quite versatile. As shown in the example, you can limit the string length, put a range on an integer or even enumerate the possible values of an input. This declarative approach offers immediate, tangible benefits over manual checks:

  1. Readability: You can understand the requirements in seconds.

  2. Compactness: It handles type checking, length limits, ranges ([18..]), and allowed values ({...}) in a single line.

  3. Sanitization: A good declarative system (like this one) will also automatically convert inputs (e.g., transforming "20" into the integer 20) and apply defaults.

When input data reaches the core logic, you are guaranteed that input is valid. You never have to write an if statement for validation again.

You Don't Really Need A Framework For This

You might be thinking, "The syntax is nice but my framework does not support that."

The good news is you don't need a specific framework to adopt this philosophy. You can write simple utilities to mimic this behavior. It might not be as powerful as the checkIn example above, but it can still alleviate the pain of manual checks to a certain extend. If your utility only implements part of the features demonstrated above, it will still save you lots of time and make your code more readable.

Let's try to implement a "checkIn-lite" (simplified version) in different languages:

1. JavaScript / Node.js

You can create a helper function that takes a schema object and validates your input against it.

Note: These code examples are for reference only. There may be more efficient or elegant ways to achieve the same result.

// 1. Define your Contract (Declarative)
const userSchema = {
    username: { '@type': 'string', '@required': true },
    age: { '@type': 'integer', '@required': true }
};

function  verifyInput(input, userSchema)  {
    const  errors = [],
           verified = {}

    Object.keys(userSchema).forEach(key => {
        let  spec = userSchema[key],
             value = input[key];

        // deal with '@required'  and '@default'
        if (spec['@required'])  {
            if (value === undefined)  {
                errors.push(`${key} is required`);
                return;
            }
        } else  {
            if (value === undefined && spec['@default'] !== undefined)
                value = spec['@default']
        }

        // type checking
        if (value !== null && value !== undefined)  {
            switch (spec['@type'])  {
                case  'integer':
                    if (!Number.isInteger(value))
                        errors.push(`${key} should be an integer`);
                    esle
                        verified[key] = value;          
                    break;

                case  'string':
                    if (typeof value !== 'string')
                        errors.push(`${key} should be a string`);
                    esle
                        verified[key] = value;
                    break;

                default:
                    verified[key] = value;
                    break;
            }
        }
    })

    return  {input: verified, errors: errors};
}
Enter fullscreen mode Exit fullscreen mode

2. PHP

In PHP, we can create a similar utility using an associative array as the schema.

$userSchema = [
    'username' => ['@type' => 'string', '@required' => true],
    'age'      => ['@type' => 'integer', '@required' => true]
];

function verifyInput(array $input, array $schema): array {
    $errors = [];
    $verified = [];

    foreach ($schema as $key => $spec) {
        $value = $input[$key] ?? null;

        // 1. Handle @required and @default
        if (!empty($spec['@required'])) {
            if ($value === null) {
                $errors[] = "$key is required";
                continue;
            }
        } else {
            if ($value === null && isset($spec['@default'])) {
                $value = $spec['@default'];
            }
        }

        // 2. Check Data Type
        if ($value !== null) {
            switch ($spec['@type']) {
                case 'integer':
                    if (!is_int($value))
                        $errors[] = "$key should be an integer";
                    else
                        $verified[$key] = $value;
                    break;
                case 'string':
                    if (!is_string($value))
                        $errors[] = "$key should be a string";
                    else
                        $verified[$key] = $value;
                    break;
                default:
                    $verified[$key] = $value;
                    break;
            }
        }
    }

    return ['input' => $verified, 'errors' => $errors];
}
Enter fullscreen mode Exit fullscreen mode

3. Python

Python dictionaries are perfect for this. We can write a function that accepts the input dictionary and the schema.

# 1. Define your Contract
user_schema = {
    'username': {'@type': str, '@required': True},
    'age':      {'@type': int, '@required': True}
}

def verify_input(input_data, schema):
    errors = []
    verified = {}  # Initialize the verified dictionary

    for key, spec in schema.items():
        value = input_data.get(key)

        # 1. Handle @required and @default first
        if spec.get('@required'):
            if value is None:
                errors.append(f"{key} is required")
                continue  # Skip further checks for this field
        else:
            if value is None and '@default' in spec:
                value = spec['@default']

        # 2. Check Data Type (only if value exists)
        if value is not None:
            expected_type = spec.get('@type')
            if expected_type == int and (not isinstance(value, int) or isinstance(value, bool)):
                errors.append(f"{key} should be an integer")
            elif expected_type == str and not isinstance(value, str):
                errors.append(f"{key} should be a string")
            else:
                verified[key] = value  # Only add when validation passes

    return {'input': verified, 'errors': errors}
Enter fullscreen mode Exit fullscreen mode

The "Full Blown" Experience with WNode

While the DIY solutions above are helpful, writing your own validation engine can be a project in itself. If you really want to experience the full power of declarative validation without sinking in your own energy too much, you can look for WNode for the full power of the checkIn coding pattern.

In WNode, you can declare compound data types such as arrays, objects, even array of objects or objects of arrays. If you're interested in the full feature of checkIn, you can check out the WNode CheckIn Documentation. The checkIn gatekeeper is designed in WNode to make sure a web component has a clear, well-specified input contract. With a clear input contract, WNode further asserts that input is the state for every web component to simplify state management. That design decision eventually allows WNode to avoid the client hydration issues in modern web frameworks.

I think it's important to note that you don't have to rely on frameworks (including WNode) to make declarative input contracts possible. Developers are good at building things. You can create your own version of checkIn-lite to fit your own needs.

Top comments (0)