Validation
What is Validation?
Validation ensures an input data conforms to a schema or, in general, a set of rules. Validation is a gate that allows only expected data to pass and continuing the execution.
Validation is an essential tool when data crosses different boundaries and to check predefined expectations are met.
Because an httpc server exposes functions, a special focus is put on function arguments validation.
@httpc/kit provides the Validate
middleware to easily check function parameters.
Core components
Validate
middleware
To perform function arguments validation, you can use the Validate
middleware.
You can apply the Validate
middleware to a function and specify in order the schema for each argument the function defines.
import { httpCall, Validate } from "@httpc/kit";
const add = httpCall(
Validate(Number, Number),
async (x: number, y: number) {
// here x and y are validated
}
);
In the previous example, the schema Number
is being used by the NativeTypeValidator to check the relative argument against the primitive type Number
.
httpc/kit provides multiple validators. Each one accepts a specific schema to perform the validation. By default NativeTypeValidator and PredicateValidator are enabled. You can also add 3rd party validators using popular validation packages.
While you can specify one schema per argument, you can mix different schema types for different arguments.
import { httpCall, Validate } from "@httpc/kit";
const sentNotification = httpCall(
Validate(
Number,
priority => priority === "normal" || priority === "important",
String,
),
async (destination: number, priority: string, message: string) {
// here arguments are validated
}
);
In the previous example:
destination
is validated by theNativeTypeValidator
, checking it is anumber
priority
is validated by thePredicateValidator
, running the functionmessage
is validated by theNativeTypeValidator
, checking it is astring
When the validation fails, that is, at least one argument doesn’t pass the validation, the Validate
middleware throws a ValidationError
, which translates into an HTTP 400 response.
ValidationService
service
Behind the scenes, the Validate
middleware uses the builtin ValidationService
. The ValidationService
manages all the enabled validators and runs the right one for the relative schema.
Usually you don’t need to interact directly with the ValidationService
, but if you need to perform manual validation, you can resolve it via injection:
import { useInjected, ValidationService } from "@httpc/kit";
import { ProfileSchema } from "./schemas";
function manualValidation(profile: unknown) {
const validation = useInjected(ValidationService);
const result = validation.validate(profile, ProfileSchema);
if (result.error) {
throw new ValidationError(result.errors);
}
return result.object as ProfileSchema;
}
Builtin validators
@httpc/kit provides two first-party validators:
- NativeTypeValidator to validate against primitive types
- PredicateValidator to do imperative validation via functions
Both validators are enabled by default. Which means, you can use both to validate function arguments via Validate
middleware.
Check out 3rd party validators for builtin integrations with popular libraries.
NativeTypeValidator
The NativeTypeValidator offers a quick but effective way to perform validation against primitive types.
You can use javascript native object as schema to validate function arguments. The supported native object are:
String
Number
Boolean
Date
Object
Array
import { httpCall, Validate } from "@httpc/kit";
const add = httpCall(
Validate(Number, Number),
async (x: number, y: number) {
// here x and y are validated
}
);
const great = httpCall(
Validate(String, Boolean),
async (name: string, isFirstTime: boolean) {
// here name and isFirstTime are validated
}
);
The NativeTypeValidator
goes a little beyond the strict checking. For some types it tries to parse them into the target native type:
Number
: if the input is astring
and it is recognized asnumber
Date
: if the input is astring
with a valid date formatBoolean
: if the input is astring
with a lowercase value oftrue
orfalse
You can disable this extended behavior, by setting the option:
import { NativeTypeValidator, NativeTypeValidatorOptions, REGISTER_OPTIONS } from "@httpc/kit";
REGISTER_OPTIONS<NativeTypeValidatorOptions>(NativeTypeValidator, {
disableParsing: true
});
PredicateValidator
The PredicateValidator
allows to validate arguments with custom code. This validation is activated when the schema is a function
.
const divideBy = httpCall(
Validate(
// use the NativeType validation for `dividend`
Number,
// use a custom function to validate `divisor`
divisor => typeof divisor === "number" && divisor > 0,
),
(dividend: number, divisor: number) => {
return dividend / divisor;
}
);
The validating function
, aka the predicate, accepts the argument and returns the result of the validation.
type Predicate = (argument: unknown) => boolean | ValidationResult
The result can be:
true
: the validation succeeded, the argument is used as isfalse
: the validation failedValidationResult
: allows to specify a custom message when the validation fails, or to overwrite the argument if passes (useful when parsing is involved)
3rd party validators
@httpc/kit offers some pre-configured integration with 3rd party libraries.
Usually a 3rd party validator can be enabled with a single import:
import "@httpc/kit/validation-*";
where the *
is the 3rd party package name.
The pre-configured validator libraries are:
class-validator
@httpc/kit provides out of the box an integration with the class-validator package.
To enable the integration, use the import:
import "@httpc/kit/validation-class";
And to do parameter validation, use the Validate
middleware:
import { httpCall, Validate } from "@httpc/kit";
import { ProfileSchema } from "./schemas";
const updateProfile = httpCall(
Validate(ProfileSchema),
async (profile: ProfileSchema) => {
// here the profile is validated
}
);
import { Length, IsEmail, IsOptional, IsString } from "class-validator";
export class ProfileSchema {
@Length(10, 20)
username: string;
@IsEmail()
email: string;
@IsOptional()
@IsString()
website?: strict;
}
Options
The class-validator
integration is pre-configured with the default options:
import { ValidatorOptions } from "class-validator";
const DEFAULT_OPTIONS: ValidatorOptions = {
whitelist: true, // allows only schema-defined property, stripping all the rest
}
For more details, checkout the class-validator documentation;
You can override the defaults with a new configuration. For convenience, the integration provides the ClassValidatorOptions
type as an alias to the ValidatorOptions
.
In your import file:
import { REGISTER_OPTIONS } from "@httpc/kit";
import "@httpc/kit/validation-class";
import { ClassValidator, ClassValidatorOptions } from "@httpc/kit/validation-class";
REGISTER_OPTIONS<ClassValidatorOptions>(ClassValidator, {
// custom configuration
whitelist: true,
});
Standalone use
If you need to run a validation manually, you can get the class-validation
validator via injection.
import { useInjected, ValidationError } from "@httpc/kit";
import { ClassValidator } from "@httpc/kit/validation-class";
import { ProfileSchema } from "./schemas";
function manualValidation(profile: any) {
const validator = useInjected(ClassValidator);
const result = validator.validate(profile, ProfileSchema);
if (!result.success) {
throw new ValidationError(result.errors);
}
return result.object as ProfileSchema;
}
zod
@httpc/kit provides out of the box an integration with the zod package.
To enable the integration, use the import:
import "@httpc/kit/validation-zod";
And to do parameter validation, use the Validate
middleware:
import { httpCall, Validate } from "@httpc/kit";
import { z } from "zod";
const ProfileSchema = z.object({
username: z.string().min(10).max(20),
email: z.string().email()
website: z.string().optional()
});
const updateProfile = httpCall(
Validate(ProfileSchema),
async (profile: z.infer<typeof ProfileSchema>) => {
// here the profile is validated
}
);
Standalone use
If you need to run a validation manually, you can get the zod
validator via injection.
import { useInjected } from "@httpc/kit";
import { ZodValidator } from "@httpc/kit/validation-class";
import { z } from "zod";
const ProfileSchema = z.object({
username: z.string().min(10).max(20),
email: z.string().email(),
website: z.string().optional(),
});
function manualValidation(order: any) {
const validator = useInjected(ZodValidator);
const result = validator.validate(order, ProfileSchema);
if (!result.success) {
throw new ValidationError(result.errors);
}
return result.object as z.infer<typeof ProfileSchema>;
}
Custom validator
// TODO
Interfaces
IValidator
// TODO