├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── examples └── react │ ├── client │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.tsx │ │ ├── User.tsx │ │ ├── api.ts │ │ └── main.tsx │ ├── tsconfig.json │ └── vite.config.ts │ ├── package.json │ ├── server │ ├── getUser.route.ts │ ├── index.ts │ ├── package.json │ ├── server.ts │ └── tsconfig.json │ └── yarn.lock ├── package.json ├── packages ├── core │ ├── .yarnrc.yml │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── RouteDefinition.ts │ │ ├── client │ │ │ └── client.ts │ │ ├── index.ts │ │ ├── providers │ │ │ ├── Provider.ts │ │ │ ├── TypeboxProvider.ts │ │ │ └── ZodProvider.ts │ │ ├── server │ │ │ └── server.ts │ │ ├── tests │ │ │ └── stack.test.ts │ │ └── types.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── yarn.lock └── react-query │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── ReactQuery.ts │ └── index.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── yarn.lock ├── site ├── .gitignore ├── README.md ├── babel.config.js ├── docs │ ├── getting-started.md │ ├── intro.mdx │ └── recipes │ │ ├── _category_.json │ │ ├── client-usage.md │ │ ├── react-query.md │ │ ├── recommended-architecture.md │ │ ├── server-usage.md │ │ └── type-inference.md ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src │ ├── components │ │ ├── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ └── Video.tsx │ └── css │ │ └── custom.css ├── static │ ├── .nojekyll │ ├── img │ │ ├── favicon+o.png │ │ ├── favicon.ico │ │ ├── favicon.png │ │ ├── favicon_o.ico │ │ ├── logo_black.png │ │ ├── logo_w.png │ │ ├── logo_white.png │ │ ├── logo_white_full.png │ │ ├── social.jpg │ │ └── social_o.jpg │ ├── presentation.mov │ └── video.mov ├── tsconfig.json └── yarn.lock └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .webpack 4 | .data 5 | build 6 | infra 7 | release.config.js 8 | .next 9 | routeTree.gen.ts 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | "ecmaVersion": "latest", 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "react", 21 | "@typescript-eslint", 22 | "simple-import-sort", 23 | "import", 24 | "unused-imports" 25 | ], 26 | "rules": { 27 | "simple-import-sort/imports": "error", 28 | "react/react-in-jsx-scope": "off", 29 | "@typescript-eslint/no-empty-function": "off", 30 | "react/display-name": 0, 31 | "@typescript-eslint/ban-ts-comment": "off", 32 | "react/prop-types": "off", 33 | "@typescript-eslint/no-empty-interface": "off", 34 | "react/no-unescaped-entities": "off", 35 | "@typescript-eslint/no-var-requires": "warn", 36 | "no-unused-vars": "off", 37 | "@typescript-eslint/no-unused-vars": "off", 38 | "unused-imports/no-unused-imports": "error", 39 | "unused-imports/no-unused-vars": [ 40 | "error", 41 | { 42 | "vars": "all", 43 | "varsIgnorePattern": "^_", 44 | "args": "after-used", 45 | "argsIgnorePattern": "^_" 46 | } 47 | ], 48 | "import/no-useless-path-segments": ["error"], 49 | "import/no-duplicates": ["error", { "considerQueryString": true }], 50 | "import/no-unassigned-import": ["off"], 51 | "import/newline-after-import": ["error", { "count": 1 }] 52 | }, 53 | "settings": { 54 | "react": { 55 | "version": "detect" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - name: Run yarn install 12 | run: yarn install 13 | 14 | - name: Run prettier and eslint 15 | run: yarn lint 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Directory for instrumented libs generated by jscoverage/JSCover 9 | lib-cov 10 | 11 | # Coverage directory used by tools like istanbul 12 | coverage 13 | *.lcov 14 | 15 | # Compiled binary addons (https://nodejs.org/api/addons.html) 16 | build/Release 17 | 18 | # Dependency directories 19 | node_modules/ 20 | 21 | # Yarn Integrity file 22 | .yarn-integrity 23 | 24 | # dotenv environment variables file 25 | .env 26 | .env.test 27 | 28 | # parcel-bundler cache (https://parceljs.org/) 29 | .cache 30 | 31 | dist 32 | 33 | .idea/ 34 | 35 | .pnp.* 36 | .yarn/* 37 | !.yarn/patches 38 | !.yarn/plugins 39 | !.yarn/releases 40 | !.yarn/sdks 41 | !.yarn/versions 42 | 43 | **/.yarn/* -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | .yarn 3 | node_modules 4 | .next 5 | 6 | styled-system 7 | styled-system/** 8 | styled-system/**/* 9 | routeTree.gen.ts 10 | build 11 | .docusaurus 12 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "printWidth": 80, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[typescriptreact]": { 4 | "editor.defaultFormatter": "esbenp.prettier-vscode" 5 | }, 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": true 8 | }, 9 | "files.insertFinalNewline": true, 10 | "files.exclude": { 11 | "**/.git": true, 12 | "**/.svn": true, 13 | "**/.hg": true, 14 | "**/CVS": true, 15 | "**/.DS_Store": true, 16 | "**/*/lib": true, 17 | "**/*/build": true, 18 | "**/.webpack": true 19 | }, 20 | "files.watcherExclude": { 21 | "**/*/lib": true, 22 | "**/*/dist": true, 23 | "**/*/build": true, 24 | "**/.webpack": true, 25 | "**/node_modules": false 26 | }, 27 | "search.exclude": { 28 | "**/*/lib": true, 29 | "**/*/dist": true, 30 | "**/*/build": true, 31 | "**/.webpack": true, 32 | "**/.next": true, 33 | "**/node_modules": true 34 | }, 35 | "javascript.preferences.importModuleSpecifier": "relative", 36 | "typescript.preferences.importModuleSpecifier": "relative" 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Florian DE LA COMBLE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

http-wizard

2 |

Typing SVG

3 | 4 | ### Full documentation website: 5 | 6 | [http-wizard.vercel.app](https://http-wizard.vercel.app) 7 | 8 | ## Introduction 9 | 10 | Http-wizard weaves TypeScript magic, offering a type-safe API client and ensuring a delightful end-to-end developer experience. ✨ 11 | 12 | #### Here is an example of usage 13 | 14 | https://github.com/flodlc/http-wizard/assets/3781663/e88fc3f8-4174-4ce0-b0f7-30ab127b4bea 15 | 16 | ### What it can do: 17 | 18 | - 100% type-safe api client with typescript magic (no code generation) 19 | - Fastify first-class support 20 | - React-query first-class support 21 | - Zod and Typebox Type providers 22 | - Delightful end-to-end developer experience (tRPC-like) 23 | - Http standards / REST compatibility: you are owner of your routes 24 | - Type inference utils 25 | 26 | --- 27 | 28 | Table of Contents: 29 | 30 | - [Installation](#installation) 31 | - [Usage](#usage) 32 | 33 | --- 34 | 35 | ## Installation 36 | 37 | To get started, install http-wizard using npm or yarn: 38 | 39 | ```sh 40 | npm install @http-wizard/core 41 | # or 42 | yarn add @http-wizard/core 43 | ``` 44 | 45 | ## Usage 46 | 47 | Currently http-wizard uses Zod or Typebox for validation. 48 | Here is an example with Zod. 49 | 50 | Let's first create a route on the server: 51 | 52 | ```typescript title="Route creation with Fastify and Zod" 53 | // server.ts 54 | import { createRoute } from '@http-wizard/core'; 55 | import { z } from 'zod'; 56 | 57 | const User = z.object({ 58 | id: z.string(), 59 | name: z.string(), 60 | }); 61 | 62 | export const getUsers = (fastify: FastifyInstance) => { 63 | return createRoute('/users', { 64 | method: 'GET', 65 | schema: { 66 | response: { 67 | 200: z.array(User), 68 | }, 69 | }, 70 | }).handle((props) => { 71 | fastify.route({ 72 | ...props, 73 | handler: (request) => { 74 | const users = await db.getUsers(); 75 | return users; 76 | }, 77 | }); 78 | }); 79 | }; 80 | 81 | const router = { ...getUsers() }; 82 | export type Router = typeof router; 83 | ``` 84 | 85 | Now, let's use the Router type on the client: 86 | 87 | ```typescript title="Client instanciation with axios" 88 | // client.ts 89 | import { createClient, ZodTypeProvider } from '@http-wizard/core'; 90 | import axios from 'axios'; 91 | 92 | import type { Router } from './server'; 93 | 94 | const apiClient = createClient(axios.instance()); 95 | const users = await apiClient.ref('[GET]/users').query({}); 96 | // users array is safe: { id:string, name:string }[] 97 | ``` 98 | 99 | Easy, right? 100 | -------------------------------------------------------------------------------- /examples/react/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/react/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview", 11 | "typecheck": "tsc" 12 | }, 13 | "dependencies": { 14 | "@http-wizard/core": "^1.3.16", 15 | "@http-wizard/react-query": "^1.3.18", 16 | "@tanstack/react-query": "^5.8.3", 17 | "axios": "^1.6.2", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "server": "*" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "^18.2.15", 24 | "@types/react-dom": "^18.2.7", 25 | "@typescript-eslint/eslint-plugin": "^6.0.0", 26 | "@typescript-eslint/parser": "^6.0.0", 27 | "@vitejs/plugin-react": "^4.0.3", 28 | "eslint": "^8.45.0", 29 | "eslint-plugin-react-hooks": "^4.6.0", 30 | "eslint-plugin-react-refresh": "^0.4.3", 31 | "typescript": "^5.1.2", 32 | "vite": "^4.4.5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/react/client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 2 | import { useState } from 'react'; 3 | 4 | import { User } from './User'; 5 | 6 | export function App() { 7 | const [queryClient] = useState(new QueryClient()); 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /examples/react/client/src/User.tsx: -------------------------------------------------------------------------------- 1 | import { api } from './api'; 2 | 3 | export const User = () => { 4 | const { data: user } = api.ref('[GET]/user').useQuery({}); 5 | 6 | if (!user) return <>loading; 7 | 8 | return ( 9 |
10 |
Name: {user.name}
11 |
Age: {user.age}
12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /examples/react/client/src/api.ts: -------------------------------------------------------------------------------- 1 | import { ZodTypeProvider } from '@http-wizard/core'; 2 | import { createQueryClient } from '@http-wizard/react-query'; 3 | import axios from 'axios'; 4 | import { Router } from 'server'; 5 | 6 | export const api = createQueryClient({ 7 | instance: axios.create(), 8 | }); 9 | -------------------------------------------------------------------------------- /examples/react/client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | 4 | import { App } from './App.tsx'; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 7 | ReactDOM.createRoot(document.getElementById('root')!).render( 8 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /examples/react/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "allowSyntheticDefaultImports": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | "esModuleInterop": true, 18 | 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "composite": true 25 | }, 26 | "include": ["src", "vite.config.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/react/client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { defineConfig } from 'vite'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /examples/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-exemple", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "start": "echo 11 && (yarn workspace server dev & yarn workspace client dev)" 8 | }, 9 | "workspaces": [ 10 | "client", 11 | "server" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /examples/react/server/getUser.route.ts: -------------------------------------------------------------------------------- 1 | import { createRoute } from '@http-wizard/core'; 2 | import { z } from 'zod'; 3 | 4 | import { Server } from './server'; 5 | 6 | export const getUserRoute = (server: Server) => { 7 | return createRoute('/user', { 8 | method: 'GET', 9 | schema: { 10 | response: { 11 | 200: z.object({ 12 | name: z.string(), 13 | age: z.number(), 14 | }), 15 | }, 16 | }, 17 | }).handle((props) => { 18 | server.route({ 19 | ...props, 20 | handler: (_, response) => { 21 | response.code(200).send({ name: 'John', age: 30 }); 22 | }, 23 | }); 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /examples/react/server/index.ts: -------------------------------------------------------------------------------- 1 | import { getUserRoute } from './getUser.route'; 2 | import { server } from './server'; 3 | 4 | const router = { ...getUserRoute(server) }; 5 | 6 | export type Router = typeof router; 7 | 8 | server.listen({ port: 5000, host: '0.0.0.0' }, () => { 9 | console.log('server listening'); 10 | }); 11 | -------------------------------------------------------------------------------- /examples/react/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.1", 4 | "private": true, 5 | "license": "MIT", 6 | "types": "./index.ts", 7 | "scripts": { 8 | "dev": "tsx watch index.ts", 9 | "start": "tsx " 10 | }, 11 | "dependencies": { 12 | "@fastify/cors": "^8.2.0", 13 | "fastify": "^4.12.0", 14 | "fastify-type-provider-zod": "^1.1.9", 15 | "@http-wizard/core": "1.3.16", 16 | "zod": "^3.22.4" 17 | }, 18 | "engines": { 19 | "node": ">=18.0.0" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^20.6.0", 23 | "tsx": "^4.1.2", 24 | "typescript": "^5.1.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/react/server/server.ts: -------------------------------------------------------------------------------- 1 | import fastifyCors from '@fastify/cors'; 2 | import fastify from 'fastify'; 3 | import { 4 | serializerCompiler, 5 | validatorCompiler, 6 | ZodTypeProvider, 7 | } from 'fastify-type-provider-zod'; 8 | 9 | export const server = fastify({ 10 | logger: true, 11 | }).withTypeProvider(); 12 | 13 | server.setValidatorCompiler(validatorCompiler); 14 | server.setSerializerCompiler(serializerCompiler); 15 | 16 | server.register(fastifyCors, {}); 17 | 18 | export type Server = typeof server; 19 | -------------------------------------------------------------------------------- /examples/react/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "esModuleInterop": true, 5 | "strict": true, 6 | "outDir": "dist" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "lint": "yarn lint:eslint && yarn lint:prettier", 4 | "lint:eslint": "eslint .", 5 | "lint:prettier": "prettier --check ." 6 | }, 7 | "devDependencies": { 8 | "typescript": "^5.0.4", 9 | "@typescript-eslint/eslint-plugin": "^5.30.0", 10 | "@typescript-eslint/parser": "^5.30.0", 11 | "eslint": "^8.51.0", 12 | "eslint-plugin-import": "^2.28.1", 13 | "eslint-plugin-react": "^7.30.1", 14 | "eslint-plugin-simple-import-sort": "^10.0.0", 15 | "eslint-plugin-unused-imports": "^3.0.0", 16 | "prettier": "^3.0.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-3.2.1.cjs 4 | -------------------------------------------------------------------------------- /packages/core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Florian DE LA COMBLE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 |

http-wizard

2 |

Typing SVG

3 | 4 | ### Full documentation website: 5 | 6 | [http-wizard.vercel.app](https://http-wizard.vercel.app) 7 | 8 | ## Introduction 9 | 10 | Http-wizard weaves TypeScript magic, offering a type-safe API client and ensuring a delightful end-to-end developer experience. ✨ 11 | 12 | #### Here is an example of usage 13 | 14 | https://github.com/flodlc/http-wizard/assets/3781663/71c749f0-3493-4865-8a9a-41421a371a05 15 | 16 | ### What it can do: 17 | 18 | - 100% type-safe api client with typescript magic (no code generation) 19 | - Fastify first-class support 20 | - React-query first-class support 21 | - Zod and Typebox Type providers 22 | - Delightful end-to-end developer experience (tRPC-like) 23 | - Http standards / REST compatibility: you are owner of your routes 24 | - Type inference utils 25 | 26 | --- 27 | 28 | Table of Contents: 29 | 30 | - [Installation](#installation) 31 | - [Usage](#usage) 32 | 33 | --- 34 | 35 | ## Installation 36 | 37 | To get started, install http-wizard using npm or yarn: 38 | 39 | ```sh 40 | npm install @http-wizard/core 41 | # or 42 | yarn add @http-wizard/core 43 | ``` 44 | 45 | ## Usage 46 | 47 | Currently http-wizard uses Zod or Typebox for validation. 48 | Here is an example with Zod. 49 | 50 | Let's first create a route on the server: 51 | 52 | ```typescript title="Route creation with Fastify and Zod" 53 | // server.ts 54 | import { createRoute } from '@http-wizard/core'; 55 | import { z } from 'zod'; 56 | 57 | const User = z.object({ 58 | id: z.string(), 59 | name: z.string(), 60 | }); 61 | 62 | export const getUsers = (fastify: FastifyInstance) => { 63 | return createRoute('/users', { 64 | method: 'GET', 65 | schema: { 66 | response: { 67 | 200: z.array(User), 68 | }, 69 | }, 70 | }).handle((props) => { 71 | fastify.route({ 72 | ...props, 73 | handler: (request) => { 74 | const users = await db.getUsers(); 75 | return users; 76 | }, 77 | }); 78 | }); 79 | }; 80 | 81 | const router = { ...getUsers() }; 82 | export type Router = typeof router; 83 | ``` 84 | 85 | Now, let's use the Router type on the client: 86 | 87 | ```typescript title="Client instanciation with axios" 88 | // client.ts 89 | import { createClient, ZodTypeProvider } from '@http-wizard/core'; 90 | import axios from 'axios'; 91 | 92 | import type { Router } from './server'; 93 | 94 | const apiClient = createClient(axios.instance()); 95 | const users = await apiClient.ref('[GET]/users').query({}); 96 | // users array is safe: { id:string, name:string }[] 97 | ``` 98 | 99 | Easy, right? 100 | -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | transform: { 4 | '^.+\\.tsx?$': 'esbuild-jest', 5 | }, 6 | collectCoverageFrom: ['src/**/*.{ts,tsx}'], 7 | testEnvironment: 'node', 8 | }; 9 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@http-wizard/core", 3 | "repository": "https://github.com/flodlc/http-wizard.git", 4 | "author": "flodlc ", 5 | "license": "MIT", 6 | "main": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "module": "./dist/index.mjs", 9 | "packageManager": "yarn@3.2.1", 10 | "version": "1.3.20", 11 | "scripts": { 12 | "dev": "tsup src/index.ts --watch", 13 | "typecheck": "tsc", 14 | "build": "tsup src/index.ts --dts", 15 | "test": "jest" 16 | }, 17 | "devDependencies": { 18 | "@sinclair/typebox": "^0.31.23", 19 | "@tanstack/react-query": "^5.8.1", 20 | "@types/jest": "^29.5.2", 21 | "@types/node": "^18.7.16", 22 | "axios": "^1.4.0", 23 | "esbuild-jest": "^0.5.0", 24 | "jest": "^29.5.0", 25 | "ts-jest": "^29.1.1", 26 | "tsup": "^6.2.3", 27 | "typescript": "^5.0.4", 28 | "zod": "^3.22.4" 29 | }, 30 | "peerDependencies": { 31 | "axios": "^1.4.0" 32 | }, 33 | "files": [ 34 | "dist/*" 35 | ], 36 | "exports": { 37 | ".": { 38 | "types": "./dist/index.d.ts", 39 | "import": "./dist/index.mjs", 40 | "require": "./dist/index.js" 41 | } 42 | }, 43 | "keywords": [ 44 | "zod", 45 | "typebox", 46 | "fastify", 47 | "typescript", 48 | "client", 49 | "api", 50 | "node", 51 | "js" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /packages/core/src/RouteDefinition.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from 'axios'; 2 | 3 | import { SchemaTypeBox } from './providers/TypeboxProvider'; 4 | import { SchemaZod } from './providers/ZodProvider'; 5 | 6 | export type Schema = SchemaTypeBox | SchemaZod; 7 | 8 | export type RouteDefinition = { 9 | method: AxiosRequestConfig['method']; 10 | url: string | (({ params }: { params: { [s: string]: string } }) => string); 11 | okCode?: number; 12 | schema: Schema; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/core/src/client/client.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance, AxiosRequestConfig } from 'axios'; 2 | 3 | import { TypeProvider } from '../providers/Provider'; 4 | import { RouteDefinition } from '../RouteDefinition'; 5 | import { Args, OkResponse } from '../types'; 6 | 7 | const processUrl = (url: string, args: object) => 8 | Object.entries(('params' in args ? args?.params : undefined) ?? {}).reduce( 9 | (acc, [key, value]) => 10 | acc.replace(new RegExp(`:${key}`, 'g'), value as string), 11 | url 12 | ); 13 | 14 | export const createRouteUri = < 15 | D extends RouteDefinition, 16 | TP extends TypeProvider, 17 | >({ 18 | method, 19 | url, 20 | instance, 21 | args, 22 | config, 23 | }: { 24 | method: AxiosRequestConfig['method']; 25 | url: string; 26 | instance: AxiosInstance; 27 | args: Args; 28 | config?: AxiosRequestConfig; 29 | }): string => { 30 | return instance.getUri({ 31 | method, 32 | url: processUrl(url, args), 33 | params: args?.query, 34 | data: args?.body, 35 | ...config, 36 | ...args, 37 | }); 38 | }; 39 | 40 | export const query = async < 41 | D extends RouteDefinition, 42 | TP extends TypeProvider, 43 | >({ 44 | method, 45 | url, 46 | instance, 47 | args, 48 | config, 49 | }: { 50 | method: AxiosRequestConfig['method']; 51 | url: string; 52 | instance: AxiosInstance; 53 | args: Args; 54 | config?: AxiosRequestConfig; 55 | }): Promise> => { 56 | const { data } = await instance.request({ 57 | method, 58 | url: processUrl(url, args), 59 | ...config, 60 | params: 'query' in args ? args.query : undefined, 61 | data: 'body' in args ? args.body : undefined, 62 | }); 63 | 64 | return data; 65 | }; 66 | 67 | export type Client< 68 | Definitions extends Record, 69 | TP extends TypeProvider, 70 | > = { 71 | ref: ( 72 | url: URL 73 | ) => Ref; 74 | infer: { 75 | [URL in keyof Definitions & string]: OkResponse; 76 | }; 77 | inferArgs: { 78 | [URL in keyof Definitions & string]: Args; 79 | }; 80 | }; 81 | 82 | export type Ref< 83 | Definitions extends Record, 84 | URL extends keyof Definitions & string, 85 | TP extends TypeProvider, 86 | > = { 87 | url: ( 88 | args: Args, 89 | config?: AxiosRequestConfig 90 | ) => string; 91 | query: ( 92 | args: Args, 93 | config?: AxiosRequestConfig 94 | ) => Promise>; 95 | }; 96 | 97 | export const createClient = < 98 | Definitions extends Record, 99 | TP extends TypeProvider, 100 | >({ 101 | instance, 102 | }: { 103 | instance: AxiosInstance; 104 | }) => { 105 | return { 106 | route: ( 107 | url: URL, 108 | args: Args, 109 | config?: AxiosRequestConfig 110 | ) => { 111 | const method = url.split(']')[0].replace('[', ''); 112 | const shortUrl = url.split(']').slice(1).join(']'); 113 | return { 114 | url: createRouteUri({ 115 | url: shortUrl, 116 | method, 117 | instance, 118 | args, 119 | config, 120 | }), 121 | query: () => { 122 | return query({ 123 | url: shortUrl, 124 | method, 125 | instance, 126 | args, 127 | config, 128 | }); 129 | }, 130 | }; 131 | }, 132 | ref: ( 133 | url: URL 134 | ): Ref => { 135 | const method = url.split(']')[0].replace('[', ''); 136 | const shortUrl = url.split(']').slice(1).join(']'); 137 | return { 138 | url: ( 139 | args: Args, 140 | config?: AxiosRequestConfig 141 | ) => 142 | createRouteUri({ 143 | url: shortUrl, 144 | method, 145 | instance, 146 | args, 147 | config, 148 | }), 149 | query: ( 150 | args: Args, 151 | config?: AxiosRequestConfig 152 | ) => { 153 | return query({ 154 | url: shortUrl, 155 | method, 156 | instance, 157 | args, 158 | config, 159 | }); 160 | }, 161 | }; 162 | }, 163 | infer: undefined as unknown as { 164 | [URL in keyof Definitions & string]: OkResponse; 165 | }, 166 | inferArgs: undefined as unknown as { 167 | [URL in keyof Definitions & string]: Args; 168 | }, 169 | }; 170 | }; 171 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { TypeProvider } from './providers/Provider'; 2 | export type { TypeBoxTypeProvider } from './providers/TypeboxProvider'; 3 | export type { ZodTypeProvider } from './providers/ZodProvider'; 4 | export type { RouteDefinition } from './RouteDefinition'; 5 | export type { OkResponse, Args } from './types'; 6 | export { createClient } from './client/client'; 7 | export { createRoute } from './server/server'; 8 | export type { Client, Ref } from './client/client'; 9 | -------------------------------------------------------------------------------- /packages/core/src/providers/Provider.ts: -------------------------------------------------------------------------------- 1 | export interface TypeProvider { 2 | readonly input: unknown; 3 | readonly output: unknown; 4 | } 5 | 6 | export type CallTypeProvider = (F & { 7 | input: I; 8 | })['output']; 9 | -------------------------------------------------------------------------------- /packages/core/src/providers/TypeboxProvider.ts: -------------------------------------------------------------------------------- 1 | import { Static, TSchema } from '@sinclair/typebox'; 2 | 3 | import { TypeProvider } from './Provider'; 4 | 5 | export type SchemaTypeBox = { 6 | params?: TSchema; 7 | querystring?: TSchema; 8 | body?: TSchema; 9 | response: Record; 10 | }; 11 | 12 | export interface TypeBoxTypeProvider extends TypeProvider { 13 | output: this['input'] extends TSchema ? Static : never; 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/providers/ZodProvider.ts: -------------------------------------------------------------------------------- 1 | import { z, ZodType } from 'zod'; 2 | 3 | import { TypeProvider } from './Provider'; 4 | 5 | ``; 6 | export type SchemaZod = { 7 | params?: z.AnyZodObject; 8 | querystring?: z.AnyZodObject; 9 | body?: z.Schema; 10 | response: Record; 11 | }; 12 | 13 | export interface ZodTypeProvider extends TypeProvider { 14 | output: this['input'] extends ZodType ? z.infer : never; 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/server/server.ts: -------------------------------------------------------------------------------- 1 | import { RouteDefinition, Schema } from '../RouteDefinition'; 2 | 3 | const methods = [ 4 | 'GET', 5 | 'POST', 6 | 'PUT', 7 | 'DELETE', 8 | 'HEAD', 9 | 'PATCH', 10 | 'OPTIONS', 11 | 'COPY', 12 | 'MOVE', 13 | 'SEARCH', 14 | ] as const; 15 | 16 | export const createRouteDefinition = ( 17 | routeDefinition: R 18 | ) => routeDefinition; 19 | 20 | export const createRoute = < 21 | const URL extends string, 22 | const D extends { 23 | schema: Schema; 24 | okCode?: number; 25 | method: (typeof methods)[number]; 26 | }, 27 | >( 28 | url: URL, 29 | options: D 30 | ) => { 31 | return { 32 | handle: ( 33 | callback: (args: { 34 | method: (typeof methods)[number]; 35 | url: URL; 36 | schema: D['schema']; 37 | }) => void 38 | ) => { 39 | callback({ 40 | url, 41 | method: options.method, 42 | schema: options.schema, 43 | }); 44 | const routeDef = { url, ...options }; 45 | const key = `${options.method}/${url}` as `${D['method']}${URL}`; 46 | return { [key]: routeDef } as { 47 | [k in `[${D['method']}]${URL}`]: typeof routeDef; 48 | }; 49 | }, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /packages/core/src/tests/stack.test.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@sinclair/typebox'; 2 | import { AxiosInstance } from 'axios'; 3 | 4 | import { createClient } from '../client/client'; 5 | import { TypeBoxTypeProvider } from '../providers/TypeboxProvider'; 6 | import { createRoute } from '../server/server'; 7 | 8 | const getUser = createRoute('/user/:id', { 9 | method: 'GET', 10 | schema: { 11 | params: Type.Object({ 12 | id: Type.String(), 13 | }), 14 | response: { 15 | 200: Type.Array( 16 | Type.Object({ 17 | name: Type.String(), 18 | age: Type.Number(), 19 | }) 20 | ), 21 | }, 22 | }, 23 | }).handle(() => {}); 24 | 25 | const getToken = createRoute('/token', { 26 | method: 'GET', 27 | schema: { 28 | querystring: Type.Object({ size: Type.String() }), 29 | response: { 30 | 200: Type.String(), 31 | }, 32 | }, 33 | }).handle(() => {}); 34 | 35 | const createUser = createRoute('/user', { 36 | method: 'POST', 37 | schema: { 38 | body: Type.Object({ name: Type.String() }), 39 | response: { 40 | 200: Type.String(), 41 | }, 42 | }, 43 | }).handle(() => {}); 44 | 45 | const routes = { ...getUser, ...getToken, ...createUser }; 46 | 47 | type Router = typeof routes; 48 | 49 | describe('Check requests parameters and response', () => { 50 | it('it should correctly call axios.request for a GET query with query parameters', async () => { 51 | const request = jest.fn((_params) => { 52 | return { data: { name: 'John Doe' } }; 53 | }); 54 | const client = createClient({ 55 | instance: { 56 | request, 57 | getUri: () => '/user/toto', 58 | } as unknown as AxiosInstance, 59 | }); 60 | 61 | const user = await client 62 | .ref('[GET]/user/:id') 63 | .query({ params: { id: 'toto' } }); 64 | 65 | const url = await client.route('[GET]/user/:id', { params: { id: 'toto' } }) 66 | .url; 67 | 68 | expect(request.mock.calls?.[0]?.[0]).toMatchObject({ 69 | url: '/user/toto', 70 | method: 'GET', 71 | }); 72 | expect(url).toBe('/user/toto'); 73 | expect(user).toMatchObject({ name: 'John Doe' }); 74 | }); 75 | 76 | it('it should correctly call axios.request with corrects parameters for a GET query without arguments', async () => { 77 | const request = jest.fn((_params) => { 78 | return { data: 'my-token' }; 79 | }); 80 | const client = createClient({ 81 | instance: { request, getUri: () => '' } as unknown as AxiosInstance, 82 | }); 83 | 84 | const token = await client 85 | .route('[GET]/token', { query: { size: '20' } }) 86 | .query(); 87 | 88 | expect(request.mock.calls?.[0]?.[0]).toMatchObject({ 89 | url: '/token', 90 | method: 'GET', 91 | params: { size: '20' }, 92 | }); 93 | expect(token).toBe('my-token'); 94 | }); 95 | 96 | it('it should correctly call axios.request on a POST query with a body', async () => { 97 | const request = jest.fn((_params) => { 98 | return { data: { name: 'John Doe' } }; 99 | }); 100 | const client = createClient({ 101 | instance: { request, getUri: () => '' } as unknown as AxiosInstance, 102 | }); 103 | 104 | const user = await client 105 | .route('[POST]/user', { 106 | body: { name: 'John Doe' }, 107 | }) 108 | .query(); 109 | 110 | expect(request.mock.calls?.[0]?.[0]).toMatchObject({ 111 | url: '/user', 112 | method: 'POST', 113 | data: { name: 'John Doe' }, 114 | }); 115 | expect(user).toMatchObject({ name: 'John Doe' }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /packages/core/src/types.ts: -------------------------------------------------------------------------------- 1 | import { CallTypeProvider, TypeProvider } from './providers/Provider'; 2 | import { RouteDefinition, Schema } from './RouteDefinition'; 3 | 4 | export type Args< 5 | S extends Schema, 6 | TP extends TypeProvider, 7 | > = (S['params'] extends object 8 | ? { params: CallTypeProvider } 9 | : { params?: undefined }) & 10 | (S['querystring'] extends object 11 | ? { query: CallTypeProvider } 12 | : { query?: undefined }) & 13 | (S['body'] extends object 14 | ? { body: CallTypeProvider } 15 | : { body?: undefined }); 16 | 17 | export type Response< 18 | S extends Schema, 19 | OK extends number, 20 | TP extends TypeProvider, 21 | > = CallTypeProvider; 22 | 23 | export type OkResponse< 24 | D extends RouteDefinition, 25 | TP extends TypeProvider, 26 | > = Response; 27 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src"], 3 | "compilerOptions": { 4 | "declarationMap": true, 5 | "listEmittedFiles": false, 6 | "listFiles": false, 7 | "pretty": true, 8 | "strictPropertyInitialization": false, 9 | "isolatedModules": true, 10 | "lib": ["ES2019"] /* Emit ECMAScript-standard-compliant class fields. */, 11 | "module": "ESNext" /* Specify what module code is generated. */, 12 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 13 | "resolveJsonModule": true, 14 | "target": "ES2015", 15 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 16 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 17 | "strict": true /* Enable all strict type-checking options. */, 18 | "skipLibCheck": false, 19 | "declaration": true, 20 | "emitDeclarationOnly": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "allowJs": false, 23 | "baseUrl": "." 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | target: 'es2015', 5 | platform: 'browser', 6 | format: ['cjs', 'esm'], 7 | splitting: false, 8 | shims: false, 9 | minify: false, 10 | sourcemap: true, 11 | clean: true, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/react-query/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Florian DE LA COMBLE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/react-query/README.md: -------------------------------------------------------------------------------- 1 |

http-wizard

2 |

Typing SVG

3 | 4 | ### Full documentation website: 5 | 6 | [http-wizard.vercel.app](https://http-wizard.vercel.app) 7 | 8 | ## Introduction 9 | 10 | Http-wizard weaves TypeScript magic, offering a type-safe API client and ensuring a delightful end-to-end developer experience. ✨ 11 | 12 | #### Here is an example of usage 13 | 14 | https://github.com/flodlc/http-wizard/assets/3781663/71c749f0-3493-4865-8a9a-41421a371a05 15 | 16 | ### What it can do: 17 | 18 | - 100% type-safe api client with typescript magic (no code generation) 19 | - Fastify first-class support 20 | - React-query first-class support 21 | - Zod and Typebox Type providers 22 | - Delightful end-to-end developer experience (tRPC-like) 23 | - Http standards / REST compatibility: you are owner of your routes 24 | - Type inference utils 25 | 26 | --- 27 | 28 | Table of Contents: 29 | 30 | - [Installation](#installation) 31 | - [Usage](#usage) 32 | 33 | --- 34 | 35 | ## Installation 36 | 37 | To get started, install http-wizard using npm or yarn: 38 | 39 | ```sh 40 | npm install @http-wizard/core 41 | # or 42 | yarn add @http-wizard/core 43 | ``` 44 | 45 | ## Usage 46 | 47 | Currently http-wizard uses Zod or Typebox for validation. 48 | Here is an example with Zod. 49 | 50 | Let's first create a route on the server: 51 | 52 | ```typescript title="Route creation with Fastify and Zod" 53 | // server.ts 54 | import { createRoute } from '@http-wizard/core'; 55 | import { z } from 'zod'; 56 | 57 | const User = z.object({ 58 | id: z.string(), 59 | name: z.string(), 60 | }); 61 | 62 | export const getUsers = (fastify: FastifyInstance) => { 63 | return createRoute('/users', { 64 | method: 'GET', 65 | schema: { 66 | response: { 67 | 200: z.array(User), 68 | }, 69 | }, 70 | }).handle((props) => { 71 | fastify.route({ 72 | ...props, 73 | handler: (request) => { 74 | const users = await db.getUsers(); 75 | return users; 76 | }, 77 | }); 78 | }); 79 | }; 80 | 81 | const router = { ...getUsers() }; 82 | export type Router = typeof router; 83 | ``` 84 | 85 | Now, let's use the Router type on the client: 86 | 87 | ```typescript title="Client instanciation with axios" 88 | // client.ts 89 | import { createClient, ZodTypeProvider } from '@http-wizard/core'; 90 | import axios from 'axios'; 91 | 92 | import type { Router } from './server'; 93 | 94 | const apiClient = createClient(axios.instance()); 95 | const users = await apiClient.ref('[GET]/users').query({}); 96 | // users array is safe: { id:string, name:string }[] 97 | ``` 98 | 99 | Easy, right? 100 | -------------------------------------------------------------------------------- /packages/react-query/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | transform: { 4 | '^.+\\.tsx?$': 'esbuild-jest', 5 | }, 6 | collectCoverageFrom: ['src/**/*.{ts,tsx}'], 7 | testEnvironment: 'node', 8 | }; 9 | -------------------------------------------------------------------------------- /packages/react-query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@http-wizard/react-query", 3 | "repository": "https://github.com/flodlc/http-wizard.git", 4 | "author": "flodlc ", 5 | "license": "MIT", 6 | "main": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "module": "./dist/index.mjs", 9 | "packageManager": "yarn@3.2.1", 10 | "version": "1.3.20", 11 | "scripts": { 12 | "dev": "tsup src/index.ts --watch", 13 | "typecheck": "tsc", 14 | "build": "tsup src/index.ts --dts", 15 | "test": "jest" 16 | }, 17 | "dependencies": {}, 18 | "devDependencies": { 19 | "@sinclair/typebox": "^0.31.23", 20 | "@tanstack/react-query": "^5.8.1", 21 | "@types/jest": "^29.5.2", 22 | "@types/node": "^18.7.16", 23 | "esbuild-jest": "^0.5.0", 24 | "@http-wizard/core": "1.3.20", 25 | "jest": "^29.5.0", 26 | "ts-jest": "^29.1.1", 27 | "tsup": "^6.2.3", 28 | "typescript": "^5.0.4", 29 | "zod": "^3.22.4", 30 | "axios": "^1.4.0" 31 | }, 32 | "peerDependencies": { 33 | "@tanstack/react-query": "5.x", 34 | "@http-wizard/core": "1.3.20" 35 | }, 36 | "files": [ 37 | "dist/*" 38 | ], 39 | "exports": { 40 | ".": { 41 | "types": "./dist/index.d.ts", 42 | "import": "./dist/index.mjs", 43 | "require": "./dist/index.js" 44 | } 45 | }, 46 | "keywords": [ 47 | "zod", 48 | "typebox", 49 | "fastify", 50 | "typescript", 51 | "client", 52 | "api", 53 | "node", 54 | "js", 55 | "react-query" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /packages/react-query/src/ReactQuery.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Args, 3 | Client, 4 | createClient, 5 | OkResponse, 6 | Ref, 7 | RouteDefinition, 8 | TypeProvider, 9 | } from '@http-wizard/core'; 10 | import { 11 | FetchQueryOptions, 12 | QueryClient, 13 | QueryKey, 14 | useInfiniteQuery, 15 | UseInfiniteQueryOptions, 16 | UseInfiniteQueryResult, 17 | useMutation, 18 | UseMutationOptions, 19 | UseMutationResult, 20 | useQuery, 21 | useQueryClient, 22 | UseQueryOptions, 23 | UseQueryResult, 24 | } from '@tanstack/react-query'; 25 | import axios, { AxiosRequestConfig } from 'axios'; 26 | 27 | export type ClientWithReactQuery< 28 | Definitions extends Record, 29 | TP extends TypeProvider, 30 | > = Omit, 'ref'> & { 31 | ref: ( 32 | url: URL 33 | ) => Ref & { 34 | useQuery: ( 35 | args: Args, 36 | options?: Omit< 37 | UseQueryOptions< 38 | OkResponse, 39 | Error, 40 | OkResponse, 41 | QueryKey 42 | >, 43 | 'queryKey' | 'queryFn' 44 | >, 45 | config?: AxiosRequestConfig 46 | ) => UseQueryResult>; 47 | useInfiniteQuery: ( 48 | args: Args, 49 | options: Omit< 50 | UseInfiniteQueryOptions>, 51 | 'queryKey' | 'queryFn' 52 | >, 53 | config?: AxiosRequestConfig 54 | ) => UseInfiniteQueryResult>; 55 | useMutation: ( 56 | options?: UseMutationOptions< 57 | OkResponse, 58 | Error, 59 | Args 60 | >, 61 | config?: Parameters['query']>[1] 62 | ) => UseMutationResult< 63 | OkResponse, 64 | Error, 65 | Args 66 | >; 67 | prefetchQuery: ( 68 | args: Args, 69 | options?: Omit< 70 | FetchQueryOptions>, 71 | 'queryKey' | 'queryFn' 72 | >, 73 | config?: Parameters['query']>[1] 74 | ) => Promise; 75 | }; 76 | }; 77 | 78 | export const createQueryClient = < 79 | Definitions extends Record, 80 | TP extends TypeProvider, 81 | >({ 82 | queryClient: optionQueryClient, 83 | ...options 84 | }: Parameters[0] & { 85 | queryClient?: QueryClient; 86 | }): ClientWithReactQuery => { 87 | const client: Client = createClient( 88 | options 89 | ); 90 | return { 91 | ...client, 92 | ref: (url: URL) => { 93 | const routeRef = client.ref(url); 94 | return { 95 | useQuery: ( 96 | args: Args, 97 | options?: Omit< 98 | UseQueryOptions< 99 | OkResponse, 100 | Error, 101 | OkResponse, 102 | QueryKey 103 | >, 104 | 'queryKey' | 'queryFn' 105 | >, 106 | config?: Parameters['query']>[1] 107 | ) => 108 | useQuery( 109 | { 110 | queryKey: [url, args], 111 | queryFn: () => routeRef.query(args, config), 112 | ...options, 113 | }, 114 | optionQueryClient 115 | ), 116 | useInfiniteQuery: ( 117 | args: Args, 118 | options: Omit< 119 | UseInfiniteQueryOptions>, 120 | 'queryKey' | 'queryFn' 121 | >, 122 | config?: Parameters['query']>[1] 123 | ) => 124 | useInfiniteQuery( 125 | { 126 | queryKey: [url, args], 127 | queryFn: () => routeRef.query(args, config), 128 | ...options, 129 | }, 130 | optionQueryClient 131 | ), 132 | useMutation: ( 133 | options?: UseMutationOptions< 134 | OkResponse, 135 | Error, 136 | Args 137 | >, 138 | config?: Parameters['query']>[1] 139 | ) => 140 | useMutation( 141 | { 142 | mutationKey: [url], 143 | mutationFn: (args) => routeRef.query(args, config), 144 | ...options, 145 | }, 146 | optionQueryClient 147 | ), 148 | prefetchQuery: ( 149 | args: Args, 150 | options?: Omit< 151 | FetchQueryOptions>, 152 | 'queryKey' | 'queryFn' 153 | >, 154 | config?: Parameters['query']>[1] 155 | ) => { 156 | const queryClient = optionQueryClient ?? useQueryClient(); 157 | return queryClient.prefetchQuery({ 158 | queryKey: [url, args], 159 | queryFn: () => routeRef.query(args, config), 160 | ...options, 161 | }); 162 | }, 163 | ...routeRef, 164 | }; 165 | }, 166 | }; 167 | }; 168 | 169 | createQueryClient({ instance: axios.create() }); 170 | -------------------------------------------------------------------------------- /packages/react-query/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { Client, RouteDefinition, OkResponse } from '@http-wizard/core'; 2 | export { createQueryClient } from './ReactQuery'; 3 | export type { ClientWithReactQuery } from './ReactQuery'; 4 | -------------------------------------------------------------------------------- /packages/react-query/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src"], 3 | "compilerOptions": { 4 | "declarationMap": true, 5 | "listEmittedFiles": false, 6 | "listFiles": false, 7 | "pretty": true, 8 | "strictPropertyInitialization": false, 9 | "isolatedModules": true, 10 | "lib": ["ES2019"] /* Emit ECMAScript-standard-compliant class fields. */, 11 | "module": "ESNext" /* Specify what module code is generated. */, 12 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 13 | "resolveJsonModule": true, 14 | "target": "ES2015", 15 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 16 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 17 | "strict": true /* Enable all strict type-checking options. */, 18 | "skipLibCheck": true, 19 | "declaration": true, 20 | "emitDeclarationOnly": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "allowJs": false, 23 | "baseUrl": "." 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/react-query/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | target: 'es2015', 5 | platform: 'browser', 6 | format: ['cjs', 'esm'], 7 | splitting: false, 8 | shims: false, 9 | minify: false, 10 | sourcemap: true, 11 | clean: true, 12 | external: ['http-wizard'], 13 | }); 14 | -------------------------------------------------------------------------------- /site/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | 23 | .pnp.* 24 | .yarn/* 25 | !.yarn/patches 26 | !.yarn/plugins 27 | !.yarn/releases 28 | !.yarn/sdks 29 | !.yarn/versions 30 | -------------------------------------------------------------------------------- /site/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /site/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /site/docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Getting started 6 | 7 | ## Installation 8 | 9 | To get started, install http-wizard using npm or yarn: 10 | 11 | ```bash title="command" 12 | npm install @http-wizard/core zod axios 13 | # or 14 | yarn add @http-wizard/core zod axios 15 | ``` 16 | 17 | ## How it works 18 | 19 | Currently http-wizard uses Zod or Typebox for validation. 20 | Here is an example with Zod. 21 | 22 | ```typescript title="Route creation with Fastify and Zod" 23 | // server.ts 24 | import { createRoute } from '@http-wizard/core'; 25 | import fastify from 'fastify'; 26 | import { 27 | serializerCompiler, 28 | validatorCompiler, 29 | ZodTypeProvider, 30 | } from 'fastify-type-provider-zod'; 31 | import { z } from 'zod'; 32 | 33 | const User = z.object({ 34 | id: z.string(), 35 | name: z.string(), 36 | }); 37 | 38 | export const getUsersRoute = (fastify: FastifyInstance) => { 39 | return createRoute('/users', { 40 | method: 'GET', 41 | schema: { 42 | response: { 43 | 200: z.array(User), 44 | }, 45 | }, 46 | }).handle((props) => { 47 | fastify.route({ 48 | ...props, 49 | handler: (request) => { 50 | const users = await db.getUsers(); 51 | return users; 52 | }, 53 | }); 54 | }); 55 | }; 56 | 57 | export const server = fastify().withTypeProvider(); 58 | server.setValidatorCompiler(validatorCompiler); 59 | server.setSerializerCompiler(serializerCompiler); 60 | export type Server = typeof server; 61 | 62 | const router = { ...getUsersRoute(server) }; 63 | export type Router = typeof router; 64 | ``` 65 | 66 | Now, let's use the Router type on the client: 67 | 68 | ```typescript title="Client instancation with axios" 69 | // client.ts 70 | import { createClient, ZodTypeProvider } from '@http-wizard/core'; 71 | import axios from 'axios'; 72 | 73 | import type { Router } from './server'; 74 | 75 | const apiClient = createClient(axios.instance()); 76 | const users = await apiClient.ref('[GET]/users').query({}); 77 | // users array is safe: { id:string, name:string }[] 78 | ``` 79 | 80 | Let's first create a route on the server: 81 | 82 | ```typescript title="Route creation with Fastify and Zod" 83 | // server.ts 84 | import { createRoute } from '@http-wizard/core'; 85 | import fastify from 'fastify'; 86 | import { 87 | serializerCompiler, 88 | validatorCompiler, 89 | ZodTypeProvider, 90 | } from 'fastify-type-provider-zod'; 91 | import { z } from 'zod'; 92 | 93 | const User = z.object({ 94 | id: z.string(), 95 | name: z.string(), 96 | }); 97 | 98 | export const getUsersRoute = (fastify: FastifyInstance) => { 99 | return createRoute('/users', { 100 | method: 'GET', 101 | schema: { 102 | response: { 103 | 200: z.array(User), 104 | }, 105 | }, 106 | }).handle((props) => { 107 | fastify.route({ 108 | ...props, 109 | handler: (request) => { 110 | const users = await db.getUsers(); 111 | return users; 112 | }, 113 | }); 114 | }); 115 | }; 116 | 117 | export const server = fastify().withTypeProvider(); 118 | server.setValidatorCompiler(validatorCompiler); 119 | server.setSerializerCompiler(serializerCompiler); 120 | export type Server = typeof server; 121 | 122 | const router = { ...getUsersRoute(server) }; 123 | export type Router = typeof router; 124 | ``` 125 | 126 | Now, let's use the Router type on the client: 127 | 128 | ```typescript title="Client instancation with axios" 129 | // client.ts 130 | import { createClient, ZodTypeProvider } from '@http-wizard/core'; 131 | import axios from 'axios'; 132 | 133 | import type { Router } from './server'; 134 | 135 | const apiClient = createClient(axios.instance()); 136 | const users = await apiClient.ref('[GET]/users').query({}); 137 | // users array is safe: { id:string, name:string }[] 138 | ``` 139 | 140 | Easy, right? 141 | -------------------------------------------------------------------------------- /site/docs/intro.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | slug: / 4 | description: Http-wizard weaves TypeScript magic, offering a type-safe API client and ensuring a delightful end-to-end developer experience. 5 | --- 6 | 7 | import { Video } from './../src/components/Video'; 8 | 9 | # Introduction 10 | 11 |
12 | 21 |
22 |
23 | Support http-wizard on github 💪 24 |
25 | 26 | #### ✨ Http-wizard weaves TypeScript magic, offering a type-safe API client and ensuring a delightful end-to-end developer experience. ✨ 27 | 28 | It natively supports Fastify interface (literally made for it) and Zod or Typebox for validation 29 | 30 | ### What it can do: 31 | 32 | - 100% type-safe api client with typescript magic (no code generation) 33 | - Fastify first-class support 34 | - React-query first-class support 35 | - Zod and Typebox Type providers 36 | - Delightful end-to-end developer experience (tRPC-like) 37 | - Http standards / REST compatibility: you are owner of your routes 38 | - Type inference utils 39 | 40 |