Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
export let action: ActionFunction = async ({ request, params }) => {
let session = await requireAuthSession(request);
await ensureUserAccount(session.get("auth"));
let data = Object.fromEntries(await request.formData());
invariant(typeof data._action === "string", "_action should be string");
switch (data._action) {
case Actions.CREATE_TASK:
case Actions.UPDATE_TASK_NAME: {
invariant(typeof === "string", "expected taskId");
invariant(typeof === "string", "expected name");
typeof === "string" || === undefined,
"expected name"
return createTask(,
case Actions.MARK_COMPLETE: {
invariant(typeof === "string", "expected task id");
return markComplete(data, id)
case Actions.MARK_INCOMPLETE: {
invariant(typeof === "string", "expected task id");
return markIncomplete(
case Actions.MOVE_TASK_TO_DAY: {
invariant(typeof === "string", "expected taskId");
invariant(, "expcted");
return moveTaskToDay(,
case Actions.MOVE_TASK_TO_BACKLOG: {
invariant(typeof === "string", "expected taskId");
return moveTaskToBacklog(,
case Actions.DELETE_TASK: {
invariant(typeof === "string", "expected taskId");
return deleteTask(
default: {
throw new Response("Bad Request", { status: 400 });
Copy link

revskill10 commented Dec 15, 2021

Joi validation integration could be nice

Copy link

renoirb commented Dec 17, 2021

This is unfinished but is a starting point of where I would handle the logic of validation separately from the place where it's executed in the controller.

That way I would be able to write tests for each validation rule, probably one "name" per need instead of per field name and type.

Also, I'd leverage a bit more user-defined assertion functions.

I would start with something like the following

 * Somewhere else, probably global to the web app

export interface IBaseViewControllerData<TData, TActions = string> {
  readonly _action: TActions;
  // Actually, this is how I would do, separate the "data" entity shape from the rest
  readonly data: TData

export type IAssertator<T> = (input: unknown | T) => asserts input is T

export type IFieldAssertator = <T>(fieldName: string, input: T | unknown) => asserts input is T 

export type IFieldValidatorReadonlyMap<T> = ReadonlyMap<string, (input: T | unknown) => asserts input is T>

The controller file

 * This controller has those possibilities
const CONTROLLER_ACTIONS = ['create','update'] as const

 * Create a type based on the CONTROLLER_ACTIONS
type IControllerActions = typeof CONTROLLER_ACTIONS[number]

 * The data this controller will work with
interface IDataEntity {
  readonly id: string
  readonly name?: string
  readonly date?: string

interface ISomeViewController extends IBaseViewControllerData<IDataEntity, IControllerActions> {
  readonly data: IDataEntity

// This should be tested.
export const fieldMap = new Map([
  ['id', (dto: IDataEntity) => Reflect.has(dto, 'id') && typeof === 'string'],
  ['name', (dto: IDataEntity) => Reflect.has(dto, 'name') && typeof === 'string'],
  ['date', (dto: IDataEntity) => Reflect.has(dto, 'date') && typeof === 'string'],
]) as IFieldValidatorReadonlyMap<IDataEntity>

 * Simple implementation of "invariant"
 * This should be tested too.
export const assertsMustHave: IFieldAssertator = (fieldName, input: unknown | IDataEntity) => {
  const maybe = fieldMap.get(fieldName as string)
  if (maybe && input && Reflect.has(input as IDataEntity, fieldName)) {
    const assertator: IAssertator<IDataEntity> = maybe
    try {
    } catch {
      const message = `Input object does not have property ${fieldName} or is not of the expected type`
      throw new TypeError(message)
  // In case trying to call a field that is not supported
  const message = `There is no field named ${fieldName}`
  throw new Error(message)

Then, in the switch map, you can do

const controllerState: ISomeViewController = { /* ... */ }
const { _action, data = {} } = controllerState

// ... then, inside the switch you can call
assertsMustHave('id', data)

Refer to:

Note that this is just a quick draft, I've litterally spent less than an hour just to see how I would write validation in a way where I'd be able to write tests without testing the controller and the data passing into it. This was just a kata to flex my muscles, I would probably do more but that was just for illustration. Clearly there has to have more validator because data types, while it is best to have flat structure, are rarely just strings numbers, boolean etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment