Skip to content

Instantly share code, notes, and snippets.

@WoLfulus
Last active February 17, 2023 00:47
Show Gist options
  • Save WoLfulus/1f4cc5da924d2697b40ad5e7313ce2e6 to your computer and use it in GitHub Desktop.
Save WoLfulus/1f4cc5da924d2697b40ad5e7313ce2e6 to your computer and use it in GitHub Desktop.
Strongly typed Next.js routes and SWR
// pages/index.tsx
import { preload } from "@routes/server";
import People from "../lib/loaders/people";
export default Index({ defaultCount }) {
const [count, setCount] = useState(defaultCount);
return (
<>
{/*
typeof count (prop) = number | undefined
autocomplete works, inferrerd from the route definition
*/}
<People.Loader count={count}>
<People.Error>Error loading people</People.Error>
<People.Loading>Loading {count} people</People.Loading>
<People.Data>
{/*
typeof list = People[]
autocomplete works, inferred from the route definition
*/}
{list => list.map(people => (
<div>{people.name} is {people.gender} and is {people.height}cm tall.</div>
))}
</People.Data>
</People>
<div>Showing {count} people.</div>
<button onClick={() => setCount(count - 1)}>Less</button>
<button onClick={() => setCount(count + 1)}>More</button>
</>
);
}
export async function getStaticProps() {
return {
props: {
defaultCount: 10,
// Plays nice with SSR/SSG/ISR with support for preloading on the server
fallback: await preload([
People({
count: defaultCount
})
])
}
}
}
// lib/loaders/people.ts
import { z, loader } from "@routes/core";
// typeof count = number | undefined, autocomplete works, inferred from route definition
export default loader("/api/people", ({ count }) => `/api/people/?count=${count}`);
// pages/api/people.ts
import { z } from "@routes/core";
import { route } from "@routes/server";
type People = {
name: string;
height: string;
gender: string;
};
export default route(
async ({ query, data }) => {
const request = await fetch("https://swapi.dev/api/people/");
const response = await request.json();
const people = (response.results as People[])
.sort(() => Math.random() - 0.5)
.slice(0, query.count) // typeof query.count = number, autocomplete works
.map(({ name, height, gender }) => ({
name,
height,
gender,
}));
return data(people);
},
{
method: "get",
query: {
count: z.coerce.number().min(0).optional().default(5),
},
}
);
// GET /api/people/
// [
// {
// "name": "Luke Skywalker",
// "height": "172",
// "gender": "male"
// },
// {
// "name": "R2-D2",
// "height": "96",
// "gender": "n/a"
// },
// {
// "name": "C-3PO",
// "height": "167",
// "gender": "n/a"
// },
// {
// "name": "Biggs Darklighter",
// "height": "183",
// "gender": "male"
// },
// {
// "name": "Beru Whitesun lars",
// "height": "165",
// "gender": "female"
// }
// ]
//
// GET /api/people/?count=2
// [
// {
// "name": "R2-D2",
// "height": "96",
// "gender": "n/a"
// },
// {
// "name": "R5-D4",
// "height": "97",
// "gender": "n/a"
// }
// ]
//
// GET /api/people/?count=-1
// {
// "error": {
// "code": "invalid_query",
// "message": "Number must be greater than or equal to 0",
// "issues": [
// {
// "code": "too_small",
// "message": "Number must be greater than or equal to 0",
// "path": "query.count"
// }
// ]
// }
// }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment