Created
February 16, 2022 15:06
-
-
Save etienne-dldc/f935c33553be2d8d8a0225f6c378fa39 to your computer and use it in GitHub Desktop.
TypeScript Typed Api Fetcher
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { z } from "zod"; | |
type Route<Result, Params> = { | |
__params: Params; | |
path: (params: Params) => string; | |
schema: z.Schema<Result>; | |
}; | |
type RoutesBase = Record<string, Route<any, any>>; | |
type FectherConfig<Routes extends RoutesBase> = { | |
baseUrl: string; | |
routes: Routes; | |
}; | |
export type FetchFn<R extends Route<any, any>> = ( | |
params: R["__params"] | |
) => Promise<z.infer<R["schema"]>>; | |
type Fetcher<Routes extends RoutesBase> = { | |
[RouteName in keyof Routes]: FetchFn<Routes[RouteName]>; | |
}; | |
export function createApiFetcher<Routes extends RoutesBase>( | |
config: FectherConfig<Routes> | |
): Fetcher<Routes> { | |
const fetcher: Fetcher<Routes> = {} as any; | |
Object.entries(config.routes).forEach(([key, route]) => { | |
(fetcher as any)[key] = async (params: any) => { | |
const path = route.path(params); | |
const response = await fetch(`${config.baseUrl}${path}`); | |
const json = await response.json(); | |
return route.schema.parse(json); | |
}; | |
}); | |
return fetcher; | |
} | |
export function route<T, Path extends string | DynamicPath<any>>( | |
path: Path, | |
schema: z.Schema<T> | |
): Route<T, Path extends DynamicPath<any> ? Parameters<Path>[0] : {}> { | |
const resolvedPath: DynamicPath<any> = | |
typeof path === "string" ? () => path : path; | |
return { | |
__params: null as any, | |
path: resolvedPath, | |
schema, | |
}; | |
} | |
type DynamicPath<Params> = (param: Params) => string; | |
export function dynamicPath<Params>( | |
fn: DynamicPath<Params> | |
): DynamicPath<Params> { | |
return fn; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createApiFetcher, dynamicPath, route } from "./apiFetcher"; | |
import { z } from "zod"; | |
const WorkoutSchema = z.object({ | |
id: z.string(), | |
date: z.string(), | |
place: z.string(), | |
distance: z.number(), | |
duration: z.number(), | |
user: z.string(), | |
placeName: z.string(), | |
speed: z.number(), | |
userName: z.string(), | |
}); | |
const WorkoutsResult = z.object({ | |
results: z.array(WorkoutSchema), | |
total: z.number(), | |
}); | |
const PlaceSchema = z.object({ | |
image: z.string(), | |
name: z.string(), | |
slug: z.string(), | |
workoutCount: z.number(), | |
}); | |
const PlacesResult = z.object({ | |
results: z.array(PlaceSchema), | |
total: z.number(), | |
}); | |
const PlaceDetails = z.object({ | |
slug: z.string(), | |
name: z.string(), | |
lng: z.number(), | |
lat: z.number(), | |
image: z.string(), | |
}); | |
type WorkoutsParams = { limit?: number; offset?: number }; | |
const workoutsFetcher = createApiFetcher({ | |
baseUrl: "http://localhost:3001", | |
routes: { | |
workouts: route( | |
(params: WorkoutsParams) => | |
`/workouts?limit=${params.limit ?? 20}&offset=${params.offset ?? 0}`, | |
WorkoutsResult | |
), | |
places: route("/places", PlacesResult), | |
place: route( | |
dynamicPath<{ placeSlug: string }>( | |
(params) => `/place/${params.placeSlug}` | |
), | |
PlaceDetails | |
), | |
}, | |
}); | |
main(); | |
async function main() { | |
const places = await workoutsFetcher.places({}); | |
console.log(places); | |
console.log(places.results[0].name); | |
const workouts = await workoutsFetcher.workouts({ offset: 20 }); | |
console.log(workouts); | |
console.log(workouts.results[0].userName); | |
const place = await workoutsFetcher.place({ | |
placeSlug: "champ-de-mars-paris", | |
}); | |
console.log(place); | |
} | |
const pokemonFetcher = createApiFetcher({ | |
baseUrl: "https://pokeapi.co/api/v2", | |
routes: { | |
pokemon: route( | |
dynamicPath<{ pokemonName: string }>( | |
(params) => `/pokemon/${params.pokemonName}` | |
), | |
z.any() | |
), | |
}, | |
}); | |
pokemonFetcher.pokemon({ pokemonName: "pikachu" }).then((pokemon) => { | |
console.log(pokemon); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment