"Router" folder
Last updated
Last updated
🎥 Watch the "router" folders section of the "Server Global State (TanStack/React Query)" style guide video here.
(OLD) Video overview ("router" folder & "route" folder): link
Our "api" object stores all of the individual "router" objects (e.g. fileCategories router, etc.), and can be imported in any component in order to access/modify any external data. Read more about the "api" object here.
For any external data, including our own backend, a "router" folder should be made.
The contents of the "router" folder are:
index.ts
Exports the "router" object (via router.ts
), as well as anything else that needs to be used outside of the "router" folder (e.g. schemas and types)
// index.ts
export * from './router';
export * from './types';
export * from './schema';
router.ts
Contains the "router" object, which has methods (procedures) for accessing/modifying the data (e.g. getSingle
, getMany
, create
, update
, delete
, etc.).
Any reusable procedure-supplying hooks (e.g. useBasicCRUDRoute
, useBasicRelationshipRoute
, etc.) are imported, and any custom procedures can be created here.
// router.ts
import { TDoc, TDocBECreate, TDocUpdate } from './types';
import { schema } from './schema';
import { adapterFn, adapterFnToBE } from './adapterFns';
import { useBasicCRUDRoute } from '../../_hooks';
// Router Config (update for each router)
export const resource = 'file-categories';
// Reusable Procedures
const basicCRUD = useBasicCRUDRoute<TDoc, TDocBECreate, TDocUpdate>(resource, adapterFn, schema, adapterFnToBE);
export const router = {
// BASIC CRUD
getSingle: basicCRUD.getSingle, // :show
getMany: basicCRUD.getMany, // :index
create: basicCRUD.create, // :create
update: basicCRUD.update, // :update
delete: basicCRUD.delete, // :destroy
// Custom Procedures
customRoute: customRoute,
};
// Custom Procedures
function customRoute(...) {...}
schema.ts
Contains source of truth for the external data's schema. We have two schema s for all external data:
"Backend" schema (TDocBE
)
the original schema of the data, as it comes from its source
"Frontend" schema (TDoc
)
the original schema converted to have all the field names be camelCase, at a minimum (see more about this in the adapter.ts
section below)
// schema.ts
import { z } from 'zod';
/**
* Frontend schema
* - these field names are used throughout the frontend
*/
export const schema = z.object({
id: z.number(),
createdAt: z.string(), // YYYY-MM-DDTHH:MM:SS.MMMZ (datetime: ISO 8601)
updatedAt: z.string(), // YYYY-MM-DDTHH:MM:SS.MMMZ (datetime: ISO 8601)
companyId: z.number(),
name: z.string(),
description: z.string(),
isActive: z.boolean(),
isDefault: z.boolean(),
});
/**
* Omit fields not required for 'create' action
*/
export const schemaCreate = schema.omit({
id: true,
createdAt: true,
updatedAt: true,
companyId: true,
isActive: true,
isDefault: true,
});
/**
* - Omit fields the user shouldn't be able to update
* - Make all fields optional (to allow partial updates)
*/
export const schemaUpdate = schema
.omit({
createdAt: true,
updatedAt: true,
})
.partial();
/**
* Backend (BE) schema
* - these field names match the external API's field names
* - if external API = our backend, then these field names should match (1) schema.rb + (2) model.rb's serializable_hash
*/
export const schemaBE = z.object({
id: z.number(),
created_at: z.string(), // YYYY-MM-DD HH:MM:SS (datetime: ISO 8601)
updated_at: z.string(), // YYYY-MM-DD HH:MM:SS (datetime: ISO 8601)
company_id: z.number(),
name: z.string(),
description: z.string(),
is_active: z.boolean(),
is_default: z.boolean(),
});
export const schemaBECreate = schemaBE.omit({
id: true,
created_at: true,
updated_at: true,
company_id: true,
is_default: true,
is_active: true,
});
export const schemaBEUpdate = schemaBE
.omit({
created_at: true,
updated_at: true,
})
.partial();
types.ts
Contains source of truth for the external data's types.
Ultimately, our types are automatically inferred from our schemas in schema.ts
, so our schemas are our ultimate single source of truth for types as well. (This is made possible by the Zod schema validation library ❤️)
// types.ts
import type { z } from 'zod';
import { schema, schemaCreate, schemaUpdate, schemaBE, schemaBECreate, schemaBEUpdate } from './schema';
export type TDoc = z.infer<typeof schema>;
export type TDocCreate = z.infer<typeof schemaCreate>;
export type TDocUpdate = z.infer<typeof schemaUpdate>;
export type TDocBE = z.infer<typeof schemaBE>;
export type TDocBECreate = z.infer<typeof schemaBECreate>;
export type TDocBEUpdate = z.infer<typeof schemaBEUpdate>;
// ...any additional types related to this resource
adapterFns.ts
Contains the adapter functions for mapping one schema to another.
Ultimately, when receive external data (our backend, external API, etc.), it should go through two steps:
(1) schema validation
This confirms the data we are receiving is of the schema we expect
const doc = schema.parse(mappedData) // this shows schema validation after mapping has already happened
(2) an adapter function.
This maps/converts the data from its original schema (original field names, original data types, etc. -- aka our schemaBE in schema.ts
) to our frontend schema (our decided upon schema for how we want to use it throughout our frontend -- aka our schema in schema.ts
)
This mapping allows us to have a consistent and reliable (frontend) schema to use throughout our app, without requiring any change if the external data's original schema changes over time. (If it does change over time, we only have to update the schemaBE and adapter functions, rather than needing to make many changes wherever this data is used throughout our app)
We have two adapter functions:
Backend-to-Frontend (BE > FE)
(read more in code sample below)
Frontend-to-Backend (FE > BE)
(read more in code sample below)
// adapterFns.ts
import { TDocBE, TDocBECreate, TDoc, TDocCreate } from './types';
/**
* Adapter function (BE > FE)
* - converts BE (backend) field names to FE (frontend) field names
* - generally, this should be a 1:1 field name mapping and should match the backend's field names, but in camelCase (standard for JS)
* - however, some of the external API's fields may be omitted via not mapping those fields to anything (case-by-case basis, as desired)
* - additionally, some of the external API's fields may be renamed (e.g. to be more consistent -- default > isDefault, etc.), combined, etc. (case-by-case basis, as desired)
*/
export function adapterFn(data: TDocBE): TDoc {
return {
id: data.id,
createdAt: data.created_at,
updatedAt: data.updated_at,
companyId: data.company_id,
name: data.name,
description: data.description,
isActive: data.is_active,
isDefault: data.is_default,
};
}
/**
* Adapter function (FE > BE)
* - converts FE (frontend) field names back to BE (backend) field names
* - this is necessary because the external data's API expects its own schema, not our contrived FE schema
* - only editable/updatable fields are included here
*/
export function adapterFnToBE(data: TDocCreate): TDocBECreate {
return {
name: data.name,
description: data.description,
is_active: data.isActive,
};
}