Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changepacks/changepack_log_MZUjkFzEPm0xEZstNHS2P.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"changes":{"packages/fetch/package.json":"Patch","packages/react-query/package.json":"Minor"},"note":"Implement react-query","date":"2025-12-02T03:13:38.013730100Z"}
91 changes: 76 additions & 15 deletions bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
coverage = true
coveragePathIgnorePatterns = ["node_modules", "**/dist/**"]
coverageSkipTestFiles = true
preload = ["./packages/react-query/setup.ts"]
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest",
"@biomejs/biome": "^2.3",
"husky": "^9"
"@testing-library/react": "^16.3.0",
"@testing-library/react-hooks": "^8.0.1",
"@types/bun": "latest",
"husky": "^9",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"author": "JeongMin Oh",
"license": "Apache-2.0",
Expand Down
1 change: 1 addition & 0 deletions packages/fetch/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as indexModule from '../index'

test('index.ts exports', () => {
expect({ ...indexModule }).toEqual({
DevupApi: expect.any(Function),
createApi: expect.any(Function),
})
})
2 changes: 1 addition & 1 deletion packages/fetch/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { getApiEndpointInfo } from './url-map'
import { getApiEndpoint, isPlainObject } from './utils'

// biome-ignore lint/suspicious/noExplicitAny: any is used to allow for flexibility in the type
type DevupApiResponse<T, E = any> =
export type DevupApiResponse<T, E = any> =
| {
data: T
error?: undefined
Expand Down
1 change: 1 addition & 0 deletions packages/fetch/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from '@devup-api/core'
export * from './api'
export { createApi } from './create-api'
251 changes: 251 additions & 0 deletions packages/react-query/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
# @devup-api/react-query

Type-safe React Query hooks built on top of `@devup-api/fetch` and `@tanstack/react-query`.

## Installation

```bash
npm install @devup-api/react-query @tanstack/react-query
```

## Prerequisites

Make sure you have `@tanstack/react-query` set up in your React application:

```tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient()

function App() {
return (
<QueryClientProvider client={queryClient}>
{/* Your app */}
</QueryClientProvider>
)
}
```

## Usage

### Create API Hooks Instance

```tsx
import { createApi } from '@devup-api/react-query'

const api = createApi('https://api.example.com', {
headers: {
'Content-Type': 'application/json'
}
})
```

### Using Query Hooks (GET requests)

```tsx
import { createApi } from '@devup-api/react-query'

const api = createApi('https://api.example.com')

function UsersList() {
// Using operationId
const { data, isLoading, error } = api.useGet('getUsers', {
query: { page: 1, limit: 20 }
})

// Using path
const { data: user } = api.useGet('/users/{id}', {
params: { id: '123' },
query: { include: 'posts' }
})

if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
if (data?.error) return <div>API Error: {data.error}</div>
if (data?.data) {
return <div>{/* Render your data */}</div>
}
return null
}
```

### Using Mutation Hooks (POST, PUT, PATCH, DELETE)

#### POST Request

```tsx
function CreateUser() {
const createUser = api.usePost('createUser')

const handleSubmit = () => {
createUser.mutate({
body: {
name: 'John Doe',
email: 'john@example.com'
}
})
}

return (
<div>
<button onClick={handleSubmit} disabled={createUser.isPending}>
{createUser.isPending ? 'Creating...' : 'Create User'}
</button>
{createUser.isError && <div>Error: {createUser.error?.message}</div>}
{createUser.data?.data && <div>Success!</div>}
</div>
)
}
```

#### PUT Request

```tsx
function UpdateUser() {
const updateUser = api.usePut('updateUser')

const handleUpdate = () => {
updateUser.mutate({
params: { id: '123' },
body: {
name: 'Jane Doe'
}
})
}

return <button onClick={handleUpdate}>Update</button>
}
```

#### PATCH Request

```tsx
function PatchUser() {
const patchUser = api.usePatch('patchUser')

const handlePatch = () => {
patchUser.mutate({
params: { id: '123' },
body: {
name: 'Jane Doe'
}
})
}

return <button onClick={handlePatch}>Patch</button>
}
```

#### DELETE Request

```tsx
function DeleteUser() {
const deleteUser = api.useDelete('deleteUser')

const handleDelete = () => {
deleteUser.mutate({
params: { id: '123' }
})
}

return <button onClick={handleDelete}>Delete</button>
}
```

### Advanced Query Options

You can pass additional React Query options to customize behavior:

```tsx
const { data, isLoading } = api.useGet(
'getUsers',
{ query: { page: 1 } },
{
staleTime: 5 * 60 * 1000, // 5 minutes
refetchOnWindowFocus: false,
retry: 3,
}
)
```

### Advanced Mutation Options

You can pass additional React Query mutation options:

```tsx
const createUser = api.usePost('createUser', {
onSuccess: (data) => {
console.log('User created:', data.data)
// Invalidate and refetch users list
queryClient.invalidateQueries({ queryKey: ['getUsers'] })
},
onError: (error) => {
console.error('Failed to create user:', error)
},
})
```

### Creating Hooks from Existing API Instance

If you already have a `DevupApi` instance from `@devup-api/fetch`, you can create hooks from it:

```tsx
import { createApi as createFetchApi } from '@devup-api/fetch'
import { createApiHooks } from '@devup-api/react-query'

const fetchApi = createFetchApi('https://api.example.com')
const api = createApiHooks(fetchApi)

// Now you can use api.useGet, api.usePost, etc.
```

## Response Handling

All hooks return React Query's standard return values, with the response data following the same structure as `@devup-api/fetch`:

```tsx
type DevupApiResponse<T, E> =
| { data: T; error?: undefined; response: Response }
| { data?: undefined; error: E; response: Response }
```

Example:

```tsx
const { data } = api.useGet('getUser', { params: { id: '123' } })

if (data?.data) {
// Success - data.data is fully typed based on your OpenAPI schema
console.log(data.data.name)
console.log(data.data.email)
} else if (data?.error) {
// Error - data.error is typed based on your OpenAPI error schemas
console.error(data.error.message)
}

// Access raw Response object
console.log(data?.response.status)
```

## API Methods

- `api.useGet(path, options, queryOptions)` - GET request hook
- `api.usePost(path, mutationOptions)` - POST request hook
- `api.usePut(path, mutationOptions)` - PUT request hook
- `api.usePatch(path, mutationOptions)` - PATCH request hook
- `api.useDelete(path, mutationOptions)` - DELETE request hook

## Type Safety

All API hooks are fully typed based on your OpenAPI schema:

- Path parameters are type-checked
- Request bodies are type-checked
- Query parameters are type-checked
- Response types are inferred automatically
- Error types are inferred automatically

## License

Apache 2.0

37 changes: 37 additions & 0 deletions packages/react-query/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@devup-api/react-query",
"version": "0.0.0",
"license": "Apache-2.0",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && bun build --target node --outfile=dist/index.js src/index.ts --production --packages=external && bun build --target node --outfile=dist/index.cjs --format=cjs src/index.ts --production --packages=external"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@devup-api/fetch": "workspace:*",
"@tanstack/react-query": ">=5.90"
},
"peerDependencies": {
"react": "*",
"@tanstack/react-query": "*"
},
"devDependencies": {
"@testing-library/react-hooks": "^8.0.1",
"@types/node": "^24.10",
"@types/react": "^19.2",
"happy-dom": "^20.0.11",
"typescript": "^5.9"
}
}
27 changes: 27 additions & 0 deletions packages/react-query/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { beforeAll } from 'bun:test'

// Setup DOM environment for React testing
if (typeof globalThis.document === 'undefined') {
// @ts-expect-error - happy-dom types
const { Window } = await import('happy-dom')
const window = new Window()
const document = window.document

// @ts-expect-error - setting global document
globalThis.window = window
// @ts-expect-error - setting global document
globalThis.document = document
// @ts-expect-error - setting global navigator
globalThis.navigator = window.navigator
// @ts-expect-error - setting global HTMLElement
globalThis.HTMLElement = window.HTMLElement
}

beforeAll(() => {
// Ensure DOM is ready
if (globalThis.document) {
const root = globalThis.document.createElement('div')
root.id = 'root'
globalThis.document.body.appendChild(root)
}
})
Loading