# Getting started
Warning: This package is still in development and features may still change, be renamed or removed.
However, we would appreciate any feedback you have on how to improve this library:
- Which features are missing?
- Which features are hard to understand or unnecessary?
- Which features need to be improved?
# Installation
Install peer dependencies:
npm install @apoyo/std
Install package:
npm install @apoyo/decoders
# Introduction
In @apoyo/decoders
, a Decoder
is an object used validate and transform an input of type I
, to an output of type O
, or an DecodeError
.
export interface Decoder<I, O> {
decode: (input: I) => Result<O, DecodeError>
}
Example: Decoder<unknown, string>
will take an input of type unknown
, and return either a string
or an DecodeError
.
# Existing decoders
A lot of the most commonly used decoders are provided by this library:
- Decoder contains general purpose helpers
- TextDecoder for string validations
- NumberDecoder for number validation
- IntegerDecoder
- BooleanDecoder
- ArrayDecoder
- ObjectDecoder
If is also very easy to create custom decoders from scratch.
# Definition
import { ObjectDecoder, ArrayDecoder, DateDecoder, IntegerDecoder, TextDecoder, Decoder, BooleanDecoder } from '@apoyo/decoders'
import { pipe, Result } from '@apoyo/std'
const validateAge = (dob: string) => {
const now = new Date()
const date = new Date(dob)
if (date.getFullYear() < now.getFullYear() - 100) {
return Result.ko(DecodeError.value(dob, 'Date of birth is more than 100 years ago'))
}
if (date.getFullYear() > now.getFullYear() - 18) {
return Result.ko(DecodeError.value(dob, 'Date of birth is less than 18 years ago'))
}
return Result.ok(dob)
}
const UserDto = ObjectDecoder.struct({
id: TextDecoder.string,
email: TextDecoder.email,
name: pipe(TextDecoder.varchar(1, 100), Decoder.nullable),
dob: pipe(DateDecoder.date, Decoder.parse(validateAge), Decoder.nullable),
age: IntegerDecoder.range(0, 120),
createdAt: DateDecoder.datetime,
updatedAt: DateDecoder.datetime
})
const TagDto = pipe(
TextDecoder.string,
TextDecoder.between(1, 32)
)
const TodoDto = ObjectDecoder.struct({
id: TextDecoder.string,
title: TextDecoder.varchar(1, 100),
done: pipe(BooleanDecoder.boolean),
// tags: string[]
tags: pipe(
ArrayDecoder.array(TagDto),
ArrayDecoder.between(0, 5),
Decoder.optional,
Decoder.map((input) => (input === undefined ? [] : input))
),
// description: string | null
description: pipe(
TextDecoder.varchar(0, 2000),
Decoder.nullable,
Decoder.optional,
Decoder.map((input) => (input === '' || input === undefined ? null : input))
),
createdAt: DateDecoder.datetime,
updatedAt: DateDecoder.datetime
})
const TodoPostDto = pipe(TodoDto, ObjectDecoder.omit(['id', 'createdAt', 'updatedAt']))
const TodoPutDto = pipe(TodoDto, ObjectDecoder.partial, ObjectDecoder.omit(['id', 'createdAt', 'updatedAt']))
interface TodoDto extends Decoder.TypeOf<typeof TodoDto> {}
interface TodoPostDto extends Decoder.TypeOf<typeof TodoPostDto> {}
interface TodoPutDto extends Decoder.TypeOf<typeof TodoPutDto> {}
# Usage
const result = pipe(
input,
Decoder.validate(TodoDto)
)
if (Result.isKo(result)) {
return new Error(`Validation failed:\n${DecodeError.draw(result.ko)}`)
}
const dto = result.ok
console.log(`Validation successful`, dto)
# Optional vs nullable
In @apoyo/decoders
, like in Typescript, we differentiate between null
and undefined
types.
As such, if you need to support both, you need to chain both helpers:
// Decoder<unknown, string | null | undefined>
const myDecoder = pipe(
TextDecoder.string,
Decoder.optional,
Decoder.nullable
)
# Example
Let's say your are implementing a HTTP REST API endpoint to create a todo list item.
You would like to validate the POST payload before creating the todo item in database.
Or return an unprocessable entity HTTP error if payload is invalid.
Here's how you can do with Apoyo's decoders:
import { DecodeError } from '@apoyo/decoders'
import { pipe, Result } from '@apoyo/std'
import { TodoModel } from './models'
export const handler = async (event: HttpEvent) => {
// Result is an union between OK and KO result
const result = pipe(
event.body,
Decoder.validate(TodoPostDto)
)
// Use type guard isKo to properly cast result
if (Result.isKo(result)) {
// If error result return "Unprocessable entity" and properly format errors with flatten
return {
status: 422,
body: DecodeError.flatten(result.ko)
}
}
// If payload is valid, save item (result.ok should have the correct type)
const saved = await TodoModel.save(result.ok)
// And return status "Created"
return {
status: 201,
body: saved,
}
}
← Seq Decode errors →