TypePath

ergonomic headless typesafe router

$ npm i typepath
get ( "/users/ :id
:id me
" )

// Think
   tRPC + REST

const r = router({ "/users/:id": get((ctx) => { const { id } = ctx.rawParams; return { name: "John Doe", id }; }) }); router.get("/users/xyz"); // { name: "John Doe", id: "xyz" }

// fits anywhere

const r = router({ ... }); // Cloudflare Worker (or other edge server) export default { fetch(req) { return r.handle(req); } } // Directly callable r.get("/me");

// use with fetch,
   ws, etc

const c = client({ fetch: myFetch }); const ws = new WebSocket("ws://localhost:3000"); const cWs = client({ send: (payload) => ws.send(payload.wire()) });

// zod validation

const r = router({ '/users': body( z.object({ name: z.string() }) ).post((ctx) => { return { name: ctx.body.name }; }) });

// validate params

const r = router({ "/list/:id": params({ id: z.string().min(3) }) .get((ctx) => { return { id: ctx.params.id }; }) });

// easy merging

const users = { "/users/:id": // ... "/users/me": // ... }; const common = { "/version": // ... }; const r = router({ ...users, ...common });

// typesafe paths

const c = client<typeof r>(); c.get("/users/123"); // ✅ c.get(`/users/${user.id}`); // ✅ c.get("/users/123?limit=10"); // ✅ c.get("/users/123/abc"); // ❌

// multiple
   methods

const r = router({ "/users/:id": [ get((ctx) => { const { id } = ctx.rawParams; return { name: "John Doe", id }; }), post((ctx) => { const { id } = ctx.rawParams; return { name: "John Doe", id }; }) ] });

// throw HTTP
   status errors

const r = router({ "/route": any((ctx) => { if (ctx.request.method === "PUT") { throw status(405, "Method Not Allowed"); } return { name: "John Doe" }; }) });

// no hidden
   calls

// Context loading (auth, data) is done on a per-route basis const withAuth = makeGuard((ctx) => { if (!ctx.request.headers.authorization) { throw status(401, "Unauthorized"); } return { user: ... }; }); const r = router({ "/me": get(withAuth((ctx) => { // ctx.user is available and typesafe return { firstName: ctx.user.firstName }; })) });

// catch all

const r = router({ "/article/...rest": get((ctx) => { const { rest } = ctx.rawParams; return subRouter.get(rest); }) });

// any type of payload

const r = router({ "/image": put((ctx) => { if (ctx.request.headers["content-type"] !== "image/png") { throw status(400, "Bad Request"); } return sharp(ctx.body).resize(200, 200).toBuffer(); }) });
Est 2024. Built with ❤️ by jrmy