
Product
Introducing Socket Scanning for OpenVSX Extensions
Socket now scans OpenVSX extensions, giving teams early detection of risky behaviors, hidden capabilities, and supply chain threats in developer tools.
@thinknimble/tn-models-fp
Advanced tools
This package attempts to approach tn-models from a functional paradigm to avoid having issues with types in classes. Thus, preventing doing weird stuff with class static fields which are undiscoverable from TS perspective.
We also prevent runtime obfuscation of fields ( this happened with classes where a field was forgotten to be declared, runtime would have no clue that the field ever was returned from the API). With this in mind, this package warns users that there is a mismatch between what the model declared was vs what the api actually returned.
The package is based in zod to replace models and fields approach from previous version
npm i @thinknimble/tn-models-fp
yarn add @thinknimble/tn-models-fp
pnpm i @thinknimble/tn-models-fp
You need a couple of things:
AxiosInstance. Either create it on the fly or provide an existing oneIG:
import axios from "axios"
import { z } from "zod"
const createShape = {
completed: z.boolean().default(false),
content: z.string().min(1),
completedDate: z.string().datetime().nullable(),
}
export const todoApi = createApi({
client: axios.create(),
baseUri: "api/todo",
models: {
create: createShape,
entity: { id: z.string().uuid(), ...createShape },
},
})
import {todoApi} from './services'
import {useMutation} from '@tanstack/react-query'
import {useState} from 'react'
import { Pagination } from '@thinknimble/tn-models-fp'
const TodoManager = () => {
const [selectedTodoId,setSelectedTodoId] = useState()
const {data: selectedTodo} = useQuery({
queryKey: ['todo',selectedTodoId],
queryFn: () => todoApi.retrieve( selectedTodoId )
})
const {mutateAsync:create} = useMutation({
mutationFn: todoApi.create
})
const [pagination, setPagination] = useState( new Pagination({ page:1 }) )
const { data:currentList } = useQuery({
queryKey: ['todo-list',page],
queryFn: () => todoApi.list( { pagination } )
})
return (
//...
)
}
A second parameter to createApi can be passed so you can create your own calls.
To do this you pass an object with the service callbacks. These should be created with createCustomServiceCall method:
First parameter are the models for your input and output shapes of the call.
Second parameter is the actual service call, this callback is powered up with multiple arguments that provide you with all the tools we think you need to make a type-safe call:
const updatePartial = createCustomServiceCall(
{
inputShape: partialUpdateShape,
outputShape: entityShape,
},
async ({ client, slashEndingBaseUri, input, utils: { toApi, fromApi } }) => {
const { id, ...rest } = toApi(input)
const res = await client.patch(`${slashEndingBaseUri}${id}`, rest)
return fromApi(res.data)
}
)
createApiThis is the main entrypoint to tn-models-fp.
An api handler can be created with or without custom service calls. Any custom call is provided with a set of utils accordingly to what they're told what the input-output is. These utils allow to convert camelCase->snake_case (toApi) as well as snake_case->camelCase (fromApi).
The result of the function call is an object that allows to do type-safe calls to the given api. It follows as closely as possible the same api as the class-based version of the library.
Sample react app: https://github.com/lakardion/ts-models-client
Snippet:
export const todoApi = createApi({
client, // AxiosInstance
baseUri, //string base uri
models: {
create: createZodRaw, // ZodRawShape
entity: entityZodRaw, // ZodRawShape
},
})
zod works as a validation library but it also provides a good set of type utilities that can be used to narrow, infer or define typescript types.
I see zod as a library that bridges the gap between typescript world and javascript world. In other words, compile-time and run-time. For this reason I thought it would fit perfectly for fulfilling the role of models in this functional approach.
Zod is going to be used both as the core tool for our type inference and as a validator parser (for snake_casing requests and camelCasing responses as well as checking whether the type received from api is the same as expected).
What we're using in this approach (and what we would require users to use) are zod raw shapes. Which in plain words are objects which values are
ZodTypeAny : pretty much anything that you can create with zod's zSample models:
import { z } from "zod"
const createShape = {
//ZodRawShape
completed: z.boolean().default(false), //ZodBoolean
content: z.string().min(1), // ZodString
completedDate: z.string().datetime().nullable(), // ZodString
extraInformation: z.object({
// ZodObject
developerUserId: z.string().uuid(), //...
reviewerUserId: z.string().uuid(),
qaUserId: z.string().uuid(),
prDetails: z.object({
url: z.string().url(),
}),
}),
}
const entityShape = { ...createZodRaw, id: z.number() }
Usually when using zod we directly create a zod schema (in any of their forms) but here we would like to be a step before the schema itself.
The reason for this decision was based on the fact that we're going to need to convert our schemas from/to camel/snake case. If we were to create a zod schema (object) we would render the shape inaccessible which would deter us from being able to swap its keys to another casing style.
IG:
const myZodObject = z.object({
// zod schema
dateOfBirth: z.string().datetime(),
email: z.string(),
age: z.number(),
}) // after declaration, the shape cannot be retrieved
const myZodShape = {
//zod shape
dateOfBirth: z.string().datetime(),
email: z.string(),
age: z.number(),
} // asking for the shape allow us to do what we please with its keys and later simply call `z.object` internally when we need the zod schema
createCustomServiceCall or cscThis function is used as a complement to createApi and allows us to create custom service calls attached to the api.
We provided multiple overloads for it to be fully type-safe and properly infer the parameters for you.
Without this function, you cannot add custom service calls. This was designed as to enforce the type safety of the custom calls.
// from tn-models-client sample repo
const deleteTodo = createCustomServiceCall(
{
inputShape: z.number(), //define your input shape (in this case is a ZodPrimitive)
},
async ({ input, client, slashEndingBaseUri }) => {
//you get your parsed input, the axios client and the base uri you defined in `createApi`
await client.delete(`${slashEndingBaseUri}${input}`)
}
)
const updatePartial = createCustomServiceCall(
{
inputShape: partialUpdateZodRaw, //you can also pass `ZodRawShape`s
outputShape: entityZodRaw,
},
async ({ client, slashEndingBaseUri, input, utils: { toApi, fromApi } }) => {
// we provide util methods to convert from and to api within your custom call so have you them in handy to use here.
const { id, ...rest } = toApi(input)
const res = await client.patch(`${slashEndingBaseUri}${id}`, rest)
return fromApi(res.data)
}
)
To add these custom calls to your created api you simply pass them as object to the second parameter in createApi
IG: (same as first createApi example but with custom calls)
export const todoApi = createApi(
{
client,
baseUri,
models: {
create: createZodRaw,
entity: entityZodRaw,
},
},
{
// object with declared custom service calls
deleteTodo,
updatePartial,
}
)
We also added a csc alias in case you feel customServiceCall is too long.
We provide a set of parameters in the custom service callback:
For this client to consume your uri strings you should either cast them as const or define them as template strings directly in the call
client.get(`${slashEndingBaseUri}`) // slashEndingBaseUri is an `as const` variable
client.get(`${slashEndingBaseUri}/ending/`) // ✅ define the template string directly in the function call
const uriSampleOutsideOfCall =`${slashEndingBaseUri}my-uri/not-const/`
client.get(uriSample)// ❌ this does not check, you'll get error, template string is already evaluated outside so it is considered `string`
const uriSampleOutsideOfCallAsConst = `${slashEndingBaseUri}my-uri/not-const/` as const
client get(uriSampleOutsideOfCallAsConst)//s checks, since it was cast as const outside of the call
inputShape you passedcreatePaginatedServiceCallAllows users to create paginated calls that are not directly related with the list endpoint of their resource. Such as when an endpoint retrieves a paginated list of things that are not exactly the resource ig: a search. You can also use this if you did not define a resource service the same way as this library expects (to have a /list endpoint).
This returns the paginated response. As of now (~2.0.0) we don't have support for filter params but will soon! #15 #32
IG
const getMatches = createPaginatedServiceCall(
{
// inputShape: someInputShape (optional)
outputShape: entityZodShape,
},
{
uri: "get-matches",
// httpMethod: 'post' (optional, default get)
}
)
const api = createApi(
//...models
,
{
//... other custom calls,
getMatches
}
)
createApiUtilsThis util allows to create the utils independently without the need of creating the api.
This is useful especially for creating remote procedure calls where no resource is strictly attached and an action is being triggered ig: call to send an email
createCollectionManagerCreates a collection manager (intended to be) equivalent to the existing class CollectionManager util.
The only required parameter is the fetchList field, which expects a reference from your list function in the created api.
const api = createApi({
//...
})
const collectionManager = createCollectionManager({
fetchList: api.list,
list: [], // your feed list, type-inferred from api.list
pagination: feedPagination, // your pagination object
filters: feedFilters, // inferred from api.list
})
//...
{
myCustomServiceCall: createCustomServiceCall({
inputShape:z.string()
outputShape:z.number()
callback: async( { client, input, utils, slashEndingBaseUri } ) =>{
const res = await client.post( slashEndingBaseUri, utils.toApi( { myCustomInput: input } )
return utils.fromApi( res.data )
}
for toApi to work properly we need to define the shape of the api call input, which in this case differs from the one that we are declaring in the inputShape.
Internally toApi parses into snake case with inputShape in mind. So we would probably want to separate these two shapes in case we don't want them to be the same
To make our life easier with versioning and releasing new versions of the packages we're using changeset.
If a PR for a feature conveys a release with it OR you want to release a version after some PRs have been merged.
From the root of the project you have to
pnpm changeset
Follow the prompts:
Commit those changes into the PR or create a PR for it and merge it.
What this will do is create a Version release PR that will allow you to confirm the release. Once that PR is merged, the github action will reach out to npm and publish the package for you.
FAQs
Utilities for building front-end models.
The npm package @thinknimble/tn-models-fp receives a total of 36 weekly downloads. As such, @thinknimble/tn-models-fp popularity was classified as not popular.
We found that @thinknimble/tn-models-fp demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 10 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Product
Socket now scans OpenVSX extensions, giving teams early detection of risky behaviors, hidden capabilities, and supply chain threats in developer tools.

Product
Bringing supply chain security to the next generation of JavaScript package managers

Product
A safer, faster way to eliminate vulnerabilities without updating dependencies