UNPKG

@assassinonz/exzodus-client

Version:

Axios wrapper with end to end type safety

150 lines (121 loc) 4.63 kB
# ExZodus ExZodus provides a type-safe Axios client wrapper and an Express router with auto-completion features backed by [Zod](https://github.com/colinhacks/zod) schemas. This project is heavily inspired by the [Zodios](https://www.zodios.org) project. ## Why ExZodus? The existence of this project is due to following factors. - The wrappers provided by the Zodios project caused heavy TS-Server performance issues leading to `Type instantiation is excessively deep and possibly infinite.ts(2589)` errors. - The api definition structure required by Zodios seems limited. ## Can this replace Zodios? Absolutely not. - If your workflow didn't encounter above mentioned problems, you should definitely use Zodios. It is well documented and established. - Zodios has many more features that won't be included in the scope of this project. ## How to use? ### 1. Installation ```bash npm i @assassinonz/exzodus ``` ### 2. Schema definition - Use `@kubb/swagger-zod` to generate the API schema using an `openapi.yaml` file or hand write it. - The API schema is typed as follows. ```typescript type Path = string; type Method = string; type Api = Record<Path, Record<Method, { request: z.ZodType | undefined; parameters: { path: z.ZodType | undefined; query: z.ZodType | undefined; header: z.ZodType | undefined; }; responses: Record<number | "default", z.ZodType>; errors: Record<number, z.ZodType>; }>>; ``` - An example schema looks as follows. ```typescript export const paths = { "/users/:id": { get: { request: undefined, parameters: { path: z.object({ "id": z.coerce.number.int() }), query: undefined, header: undefined }, responses: { 200: z.object({ "id": z.number.int(), "name": z.string() }), 404: z.object({ "message": z.string() }) default: z.object({ "id": z.number.int(), "name": z.string() }) }, errors: { 404: z.object({ "message": z.coerce.string() }) } }, } } ``` ### 3. Using ExZodusRouter ```typescript import { paths } from "../../kubb/zod/operations.js"; import { express, ExZodusRouter } from "@assassinonz/exzodus-router"; //Define extras if modification of request type is needed type Extras = { ctx?: { userId: number; } } // @kubb/swagger-zod generated API schema // ▼ const router = ExZodusRouter.new<typeof paths, Extras>(paths, { //Provide error handler for Zod errors errorHandler: (err, req, res) => { //TODO: Handle errors }, //Enable response validation to prevent unintentional data leaks attachResponseValidator: true }); // auto-complete path fully typed and validated input params (body, query, params) // ▼ ▼ ▼ router.get("/users/:id", (req, res) => { if (req.ctx === undefined) { //Allows only documented response codes //Response is typed from the body of 404 response // ▼ return res.status(404).json({ message: "Please login first" }); } else { const user = findUserById(req.ctx.userId); //Response is typed from the body of 200 response // ▼ return res.status(200).json({ id: user.id, name: user.name, password: user.password }); } }); const app = express(); app.use(express.json()); app.use("/api/v1", router); ``` ### 4. Using ExZodusClient Calling this API is now easy and has builtin autocomplete features : ```typescript import { paths } from "../../kubb/zod/operations.js"; import { ExZodusClient } from "@assassinonz/exzodus-client"; // @kubb/swagger-zod generated API schema // ▼ const client = new ExZodusClient(paths, "http://localhost:8080/api/v1"); // typed auto-complete path auto-complete params // ▼ ▼ ▼ const userResponse = await client.get("/users/:id", { path: { id: 7 } }); console.log(userResponse.data); ``` ### 5. Output This should output the following. Note the missing password field due to the `attachResponseValidator` option. ```typescript { id: 7, name: "John Doe" } ```