Skip to content

Instantly share code, notes, and snippets.

@etienne-dldc
Created February 16, 2022 15:06
Show Gist options
  • Save etienne-dldc/f935c33553be2d8d8a0225f6c378fa39 to your computer and use it in GitHub Desktop.
Save etienne-dldc/f935c33553be2d8d8a0225f6c378fa39 to your computer and use it in GitHub Desktop.
TypeScript Typed Api Fetcher
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;
}
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