diff --git a/SKILL.md b/SKILL.md index 909a975..bbd2146 100644 --- a/SKILL.md +++ b/SKILL.md @@ -15,30 +15,43 @@ description: | - Using DevupObject for type references --- -# devup-api Usage Guide +# devup-api Type-safe API client from OpenAPI. Zero generics, auto-generated types. -## Setup +## Important: No Type Assertions -### Install +**NEVER use `as` keyword with devup-api types.** The library uses two-phase typing: +- **Cold typing**: Before build, all types are `any` - no type errors possible +- **Boild typing**: After build, types are fully inferred from OpenAPI -```bash -# Core + Build Plugin (choose one) -npm install @devup-api/fetch @devup-api/vite-plugin # Vite -npm install @devup-api/fetch @devup-api/next-plugin # Next.js -npm install @devup-api/fetch @devup-api/webpack-plugin # Webpack -npm install @devup-api/fetch @devup-api/rsbuild-plugin # Rsbuild - -# Optional Integrations -npm install @devup-api/react-query @tanstack/react-query # React Query -npm install @devup-api/zod zod # Zod validation -npm install @devup-api/hookform react-hook-form zod # Hook Form -npm install @devup-api/ui @tanstack/react-query react-hook-form zod # CRUD UI +Type assertions (`as`) are unnecessary and may hide real issues. Let the generated types flow naturally. + +```ts +// WRONG +const user = result.data as User + +// CORRECT +const user = result.data // Type inferred automatically ``` -### Configure Build Tool +--- + +## Quick Setup +```bash +# Core + Plugin (pick one) +npm install @devup-api/fetch @devup-api/vite-plugin # Vite +npm install @devup-api/fetch @devup-api/next-plugin # Next.js + +# Optional +npm install @devup-api/react-query @tanstack/react-query +npm install @devup-api/zod zod +npm install @devup-api/hookform react-hook-form zod +npm install @devup-api/ui @tanstack/react-query react-hook-form zod +``` + +**Configure plugin:** ```ts // vite.config.ts import devupApi from '@devup-api/vite-plugin' @@ -49,94 +62,67 @@ import devupApi from '@devup-api/next-plugin' export default devupApi({ reactStrictMode: true }) ``` -### tsconfig.json - -```json -{ "include": ["src", "df/**/*.d.ts"] } -``` +**tsconfig.json:** Add `"df/**/*.d.ts"` to `include`. Place `openapi.json` in project root, run `npm run dev`. --- -## @devup-api/fetch — API Client - -### Create Client +## @devup-api/fetch ```ts import { createApi, type DevupObject } from '@devup-api/fetch' const api = createApi('https://api.example.com') -// or with options -const api = createApi({ baseUrl: 'https://api.example.com', headers: { 'X-Custom': 'value' } }) +// or: createApi({ baseUrl: '...', headers: {...} }) ``` -### HTTP Methods +### Requests ```ts -// GET -const users = await api.get('getUsers', { query: { page: 1, limit: 20 } }) +// GET - operationId or path +const users = await api.get('getUsers', { query: { page: 1 } }) const user = await api.get('/users/{id}', { params: { id: '123' } }) -// POST -const created = await api.post('createUser', { body: { name: 'John', email: 'john@example.com' } }) - -// PUT / PATCH / DELETE -await api.put('/users/{id}', { params: { id: '1' }, body: { name: 'Jane', email: 'jane@example.com' } }) -await api.patch('/users/{id}', { params: { id: '1' }, body: { name: 'Jane' } }) +// POST/PUT/PATCH/DELETE +await api.post('createUser', { body: { name: 'John' } }) +await api.put('/users/{id}', { params: { id: '1' }, body: {...} }) +await api.patch('/users/{id}', { params: { id: '1' }, body: {...} }) await api.delete('/users/{id}', { params: { id: '1' } }) ``` -### Response Handling +### Response ```ts const result = await api.get('getUser', { params: { id: '1' } }) - -if (result.data) { - console.log(result.data.name) // typed response -} else if (result.error) { - console.error(result.error) // typed error -} -console.log(result.response.status) // raw Response +if (result.data) console.log(result.data.name) +if (result.error) console.error(result.error) +// result.response = raw Response ``` ### DevupObject (Type References) -Use `DevupObject` directly in type annotations without redefining types: - ```ts -// Direct usage in variable declarations +// Direct type annotations without redefining const user: DevupObject['User'] = await fetchUser() -const body: DevupObject<'request'>['CreateUserBody'] = { name: 'John', email: 'john@example.com' } +const body: DevupObject<'request'>['CreateUserBody'] = {...} const error: DevupObject<'error'>['ErrorResponse'] = result.error -// Direct usage in function parameters -function displayUser(user: DevupObject['User']) { /* ... */ } +function UserCard({ user }: { user: DevupObject['User'] }) {...} -// Direct usage in component props -function UserCard({ user }: { user: DevupObject['User'] }) { /* ... */ } - -// Multi-server types +// Multi-server const product: DevupObject<'response', 'openapi2.json'>['Product'] = data ``` ### Middleware ```ts -// Auth api.use({ onRequest: async ({ request }) => { - const token = localStorage.getItem('token') - if (token) { - const headers = new Headers(request.headers) - headers.set('Authorization', `Bearer ${token}`) - return new Request(request, { headers }) - } - } -}) - -// Token Refresh -api.use({ + const headers = new Headers(request.headers) + headers.set('Authorization', `Bearer ${token}`) + return new Request(request, { headers }) + }, onResponse: async ({ request, response }) => { if (response.status === 401) { const newToken = await refreshToken() @@ -150,7 +136,7 @@ api.use({ --- -## @devup-api/react-query — React Query Hooks +## @devup-api/react-query ```ts import { createApi } from '@devup-api/fetch' @@ -163,9 +149,8 @@ const queryClient = createQueryClient(api) ### useQuery ```tsx -const { data, isLoading, error, refetch } = queryClient.useQuery( - 'get', - '/users/{id}', +const { data, isLoading, error } = queryClient.useQuery( + 'get', '/users/{id}', { params: { id: userId } }, { staleTime: 5 * 60 * 1000 } // React Query options ) @@ -175,37 +160,24 @@ const { data, isLoading, error, refetch } = queryClient.useQuery( ```tsx const mutation = queryClient.useMutation('post', 'createUser', { - onSuccess: (data) => { - tanstackQueryClient.invalidateQueries({ queryKey: ['get', 'getUsers'] }) - } + onSuccess: () => tanstackQueryClient.invalidateQueries({ queryKey: ['get', 'getUsers'] }) }) - -mutation.mutate({ body: { name: 'John', email: 'john@example.com' } }) +mutation.mutate({ body: { name: 'John' } }) ``` -### useSuspenseQuery +### useSuspenseQuery / useInfiniteQuery / useQueries ```tsx -// Use with React Suspense +// Suspense const { data } = queryClient.useSuspenseQuery('get', 'getUsers', {}) -``` - -### useInfiniteQuery - -```tsx -const { data, fetchNextPage, hasNextPage } = queryClient.useInfiniteQuery( - 'get', - 'getUsers', - { - initialPageParam: 1, - getNextPageParam: (lastPage) => lastPage.nextPage - } -) -``` -### useQueries (Parallel) +// Infinite +const { data, fetchNextPage } = queryClient.useInfiniteQuery('get', 'getUsers', { + initialPageParam: 1, + getNextPageParam: (lastPage) => lastPage.nextPage +}) -```tsx +// Parallel const results = queryClient.useQueries([ ['get', '/users/{id}', { params: { id: '1' } }], ['get', '/users/{id}', { params: { id: '2' } }], @@ -214,118 +186,66 @@ const results = queryClient.useQueries([ --- -## @devup-api/zod — Runtime Validation +## @devup-api/zod -Schemas auto-generated from OpenAPI via virtual module. +Auto-generated Zod schemas from OpenAPI via virtual module. ```ts -import { schemas, responseSchemas, requestSchemas, errorSchemas, pathSchemas } from '@devup-api/zod' +import { schemas, responseSchemas, requestSchemas, pathSchemas } from '@devup-api/zod' -// By category const userSchema = responseSchemas.User -const createUserSchema = requestSchemas.CreateUserRequest -const errorSchema = errorSchemas.ApiError - -// By path/operationId (for forms) -const schema = pathSchemas.post['createUser'] -const schema = pathSchemas.put['/users/{id}'] - -// Multi-server -const productSchema = schemas['openapi2.json'].response.Product +const createSchema = requestSchemas.CreateUserRequest +const formSchema = pathSchemas.post['createUser'] // For forms -// Validate const result = userSchema.safeParse(data) -if (result.success) { - console.log(result.data) -} else { - console.error(result.error.issues) -} - -// Type inference -import { z } from 'zod' type User = z.infer ``` --- -## @devup-api/hookform — React Hook Form Integration +## @devup-api/hookform Auto-validation with Zod schemas from OpenAPI. ```tsx -import { createApi } from '@devup-api/fetch' -import { ApiForm, useFormContext, useWatch, useFieldArray, Controller } from '@devup-api/hookform' - -const api = createApi('https://api.example.com') -``` - -### Basic Form +import { ApiForm, useFormContext } from '@devup-api/hookform' -```tsx function FormFields() { - const { register, formState: { errors, isSubmitting } } = useFormContext() + const { register, formState: { errors } } = useFormContext() return ( <> {errors.name && {errors.name.message}} - - {errors.email && {errors.email.message}} - + ) } -function CreateUserForm() { - return ( - console.log('Created:', data)} - onError={(error) => console.error('Error:', error)} - onValidationError={(errors) => console.log('Validation:', errors)} - > - - - ) -} -``` - -### Edit Form +// Create + + + -```tsx +// Edit console.log('Updated:', data)} + onSuccess={...} > ``` -### Props - -| Prop | Type | Description | -|------|------|-------------| -| `api` | `DevupApi` | API client | -| `method` | `'post' \| 'put' \| 'patch' \| 'delete'` | HTTP method | -| `path` | `string` | operationId or path | -| `requestOptions` | `{ params?, query?, headers? }` | Additional request options | -| `defaultValues` | `object` | Form default values | -| `mode` | `'onSubmit' \| 'onBlur' \| 'onChange'` | Validation mode | -| `resetOnSuccess` | `boolean` | Reset form after success | -| `onSuccess` | `(data) => void` | Success callback | -| `onError` | `(error) => void` | API error callback | -| `onValidationError` | `(errors) => void` | Validation error callback | +**Props:** `api`, `method`, `path`, `requestOptions`, `defaultValues`, `mode`, `resetOnSuccess`, `onSuccess`, `onError`, `onValidationError` --- -## @devup-api/ui — CRUD Components +## @devup-api/ui Auto-generated CRUD from OpenAPI tags. @@ -335,29 +255,23 @@ Auto-generated CRUD from OpenAPI tags. paths: /users/{id}: get: - tags: [devup:user:one] # GET single (required) + tags: [devup:user:one] # Required: GET single put: - tags: [devup:user:edit] # PUT update + tags: [devup:user:edit] # Optional: PUT update patch: - tags: [devup:user:fix] # PATCH update + tags: [devup:user:fix] # Optional: PATCH update /users: post: - tags: [devup:user:create] # POST create (required) + tags: [devup:user:create] # Required: POST create ``` ### Usage ```tsx -import { createApi } from '@devup-api/fetch' import { ApiCrud } from '@devup-api/ui' import { crudConfigs } from '@devup-api/ui/crud' -const api = createApi('https://api.example.com') -``` - -### Create Mode (no params) - -```tsx +// Create mode (no params) console.log('Created:', data)} /> -``` -### Edit Mode (with params) - -```tsx +// Edit mode (with params) Loading...} - oneFallback={
Not found
} onUpdateSuccess={(data) => console.log('Updated:', data)} /> -``` -### Headless Mode (Render Function) - -```tsx +// Headless mode - {({ form, mode, submit, isLoading, one }) => ( + {({ form, mode, submit, isLoading }) => (
{ e.preventDefault(); submit() }}> - - +
)}
``` -### Custom Renderers - -```tsx - ( -
- - -
- )} - renderSubmit={({ isLoading, mode }) => ( - - )} -/> -``` - ### useApiCrud Hook ```tsx -import { useApiCrud } from '@devup-api/ui' - const crud = useApiCrud({ config: crudConfigs.user, api, params: userId ? { id: userId } : undefined, - onCreateSuccess: (data) => console.log('Created:', data), - onUpdateSuccess: (data) => console.log('Updated:', data), + onCreateSuccess: (data) => {...}, + onUpdateSuccess: (data) => {...}, }) - -// crud.mode: 'create' | 'edit' -// crud.form: UseFormReturn -// crud.one: { data, isLoading, isError } -// crud.create: { mutate, isPending } -// crud.update: { mutate, isPending } -// crud.submit: () => void -// crud.isLoading: boolean +// crud.mode, crud.form, crud.one, crud.create, crud.update, crud.submit, crud.isLoading ``` -### Field Types - -`text` | `number` | `email` | `password` | `url` | `tel` | `textarea` | `select` | `checkbox` | `radio` | `date` | `datetime` | `time` | `file` | `hidden` | `array` | `object` +**Field types:** `text` | `number` | `email` | `password` | `textarea` | `select` | `checkbox` | `radio` | `date` | `datetime` | `file` | `array` | `object` --- -## Multiple API Servers +## Multi-Server ```ts -// Plugin config devupApi({ openapiFiles: ['openapi.json', 'openapi2.json'] }) -// Usage -const api1 = createApi({ baseUrl: 'https://api1.com' }) -const api2 = createApi({ baseUrl: 'https://api2.com', serverName: 'openapi2.json' }) - -// Types - use directly without redefining -const user: DevupObject['User'] = data // openapi.json -const product: DevupObject<'response', 'openapi2.json'>['Product'] = data // openapi2.json +const api2 = createApi({ baseUrl: '...', serverName: 'openapi2.json' }) +const product: DevupObject<'response', 'openapi2.json'>['Product'] = data ``` --- @@ -478,37 +346,11 @@ interface DevupApiOptions { --- -## Common Patterns - -### Request Cancellation - -```ts -const controller = new AbortController() -setTimeout(() => controller.abort(), 5000) -await api.get('getUsers', { signal: controller.signal }) -``` - -### File Upload - -```ts -const formData = new FormData() -formData.append('file', file) -await api.post('/upload', { body: formData }) -``` - -### Environment URL - -```ts -const api = createApi(import.meta.env.VITE_API_URL || 'http://localhost:3000') -``` - ---- - ## Troubleshooting | Issue | Solution | |-------|----------| | Types not appearing | Run `npm run dev`, check tsconfig includes `df/**/*.d.ts` | -| operationId not found | Use path `/users/{id}` or verify openapi.json operationId | -| Zod schemas empty | Ensure bundler plugin is configured, run dev server | -| CRUD config missing | Add `devup:{name}:one` and `devup:{name}:create` tags to OpenAPI | +| operationId not found | Use path `/users/{id}` or verify openapi.json | +| Zod schemas empty | Ensure bundler plugin is configured | +| CRUD config missing | Add `devup:{name}:one` and `devup:{name}:create` tags |