Skip to content

Instantly share code, notes, and snippets.

@vastus
Created September 18, 2025 18:00
Show Gist options
  • Select an option

  • Save vastus/c93c8bd63a9caa330f1a810e278b498f to your computer and use it in GitHub Desktop.

Select an option

Save vastus/c93c8bd63a9caa330f1a810e278b498f to your computer and use it in GitHub Desktop.
Zod usage
import {Result} from '@badrap/result'
import {ErrorMeta, NotFoundError, StatusError} from 'app/errors'
import {has, merge} from 'lodash/fp'
import {z} from 'zod'
export const zAPIError = z.object({
error: z.object({
message: z.string().min(1),
meta: z.record(z.string(), z.unknown()).nullish(),
}),
})
export type APIError = z.infer<typeof zAPIError>
export const zAPISpace = z.object({
space_id: z.string(),
name: z.string(),
})
export type APISpace = z.infer<typeof zAPISpace>
export const zAPIUser = z.object({
user_id: z.string(),
email: z.string(),
name: z.string(),
// inserted_at: z.string().datetime(),
// updated_at: z.string().datetime(),
})
export type APIUser = z.infer<typeof zAPIUser>
export const zAPIMessage = z.object({
user_id: z.string(),
channel_id: z.string(),
message_id: z.string(),
content: z.string(),
inserted_at: z.string().datetime(),
updated_at: z.string(),
user: zAPIUser.pick({email: true, name: true}),
})
export type APIMessage = z.infer<typeof zAPIMessage>
export const zAPIAuthWebTokenResp = z.object({
token: z.string().min(3),
})
export type APIAuthWebTokenResp = z.infer<typeof zAPIAuthWebTokenResp>
export const zAPIChannel = z.object({
space_id: z.string(),
channel_id: z.string(),
name: z.string(),
// inserted_at: z.string().datetime(),
// updated_at: z.string().datetime(),
})
export type APIChannel = z.infer<typeof zAPIChannel>
export const zAPISpaceList = z.array(zAPISpace)
export type APISpaceList = z.infer<typeof zAPISpaceList>
export const zAPISpaceShow = zAPISpace.extend({
channels: z.array(zAPIChannel),
})
export type APISpaceShow = z.infer<typeof zAPISpaceShow>
export async function doFetch<T extends z.ZodType>(
url: string,
schema: T,
init: RequestInit = {},
): Promise<Result<z.infer<T>>> {
const response = await fetch(url, init)
// API must always return JSON
const body = await response.json()
if (!response.ok) {
const parsed = zAPIError.safeParse(body)
if (!parsed.success) {
return Result.err(
new ErrorMeta(`request failed: unable to parse API error`, {
parsedData: body,
parseError: parsed.error,
}),
)
}
const status = response.status
const {message, meta} = parsed.data.error
if (status === 404) {
return Result.err(new NotFoundError(message, meta || {}))
}
return Result.err(new StatusError(status, message, meta || {}))
}
const parsed = schema.safeParse(body.data)
if (!parsed.success) {
return Result.err(parsed.error)
}
return Result.ok(parsed.data)
}
export async function getJSON<T extends z.ZodType>(
url: string,
schema: T,
init: RequestInit = {},
): Promise<Result<z.infer<T>>> {
init = merge(
{
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
},
init,
)
return await doFetch(url, schema, init)
// if (!response.ok) {
// const status = response.status
// const text = await response.text()
// return Result.err(new Error(`failed to GET ${url}: ${status} ${text}`))
// }
// const body = await response.json()
// const parsed = schema.safeParse(body.data)
// if (!parsed.success) {
// return Result.err(parsed.error)
// }
// return Result.ok(parsed.data)
}
export async function post<T extends z.ZodType>(
url: string,
schema: T,
init: RequestInit = {},
): Promise<Result<z.infer<T>>> {
init.method = 'POST'
const response = await fetch(url, init)
if (!response.ok) {
const status = response.status
const text = await response.text()
return Result.err(new Error(`failed to POST ${url}: ${status} ${text}`))
}
const body = await response.json()
if (!has('data', body)) {
return Result.err(new Error(`no 'data' key in body`))
}
const parsed = schema.safeParse(body.data)
if (!parsed.success) {
return Result.err(parsed.error)
}
return Result.ok(parsed.data)
}
export async function postJSON<T extends z.ZodType>(
data: unknown,
url: string,
schema: T,
init: RequestInit = {},
): Promise<Result<z.infer<T>>> {
init = merge({headers: {'content-type': 'application/json'}}, init)
init.method = 'POST'
init.body = JSON.stringify(data)
return await post(url, schema, init)
}
export async function postForm<T extends z.ZodType>(
data: FormData,
url: string,
schema: T,
init: RequestInit = {},
): Promise<Result<z.infer<T>>> {
init.method = 'POST'
init.body = data
return await post(url, schema, init)
}
import {Result} from '@badrap/result'
import {z} from 'zod'
import {config} from 'app/config'
import {
getJSON,
postJSON,
zAPIAuthWebTokenResp,
zAPIChannel,
zAPIMessage,
zAPISpace,
zAPISpaceList,
zAPISpaceShow,
zAPIUser,
type APIAuthWebTokenResp,
type APIChannel,
type APIMessage,
type APISpace,
type APISpaceList,
type APISpaceShow,
type APIUser,
} from './http'
import {isNil} from 'lodash/fp'
const {API_URL} = config()
const zOAuthProvider = z.enum(['github', 'google'])
export type OAuthProvider = z.infer<typeof zOAuthProvider>
export function mkURL(path: string) {
return [API_URL, path].join('')
}
export async function getMessagesByChannel(
token: string,
params: {
channel_id: string
earliest_message_id?: string
},
): Promise<Result<APIMessage[]>> {
const {channel_id, earliest_message_id} = params
let url = mkURL(`/v1/channels/${channel_id}/messages`)
if (!isNil(earliest_message_id)) {
url += `?earliest_message_id=${earliest_message_id}`
}
const schema = z.array(zAPIMessage)
const messagesR = await getJSON(url, schema, {
headers: {authorization: `Bearer ${token}`},
})
return messagesR
}
export async function getMe(token: string): Promise<Result<APIUser>> {
const url = mkURL(`/v1/users/me`)
return await getJSON(url, zAPIUser, {
headers: {authorization: `Bearer ${token}`},
})
}
export async function sendMessage(
token: string,
params: any,
): Promise<Result<APIMessage>> {
const {channel_id} = params
const url = mkURL(`/v1/channels/${channel_id}/messages`)
const messageR = await postJSON(params, url, zAPIMessage, {
headers: {authorization: `Bearer ${token}`},
})
return messageR
}
export async function auth(params: {
provider: OAuthProvider
code: string
}): Promise<Result<APIAuthWebTokenResp>> {
const {code, provider} = params
const url = mkURL(`/v1/auth/${provider}/token`)
return await postJSON({code}, url, zAPIAuthWebTokenResp)
}
export async function getChannels(
token: string,
params: {space_id: string},
): Promise<Result<APIChannel[]>> {
const {space_id} = params
const url = mkURL(`/v1/spaces/${space_id}/channels`)
const schema = z.array(zAPIChannel)
const result = await getJSON(url, schema, {
headers: {authorization: `Bearer ${token}`},
})
return result
}
export async function scopedSpaces(
token: string,
): Promise<Result<APISpaceList>> {
const url = mkURL(`/v1/spaces`)
const schema = zAPISpaceList
const result = await getJSON(url, schema, {
headers: {authorization: `Bearer ${token}`},
})
return result
}
export async function getSpace(
token: string,
params: Pick<APISpace, 'space_id'>,
): Promise<Result<APISpaceShow>> {
const url = mkURL(`/v1/spaces/${params.space_id}`)
const schema = zAPISpaceShow
const result = await getJSON(url, schema, {
headers: {authorization: `Bearer ${token}`},
})
return result
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment