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
6 changes: 6 additions & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ concurrency:
jobs:
build:
name: Build documentation
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 15

Expand All @@ -44,6 +45,10 @@ jobs:
if: inputs.build-artifact == ''
uses: ./.github/actions/setup-repository

- name: Check public API and documentation examples
if: inputs.build-artifact == ''
run: vp run api-types

- name: Build documentation
if: inputs.build-artifact == ''
run: vp run docs-build
Expand All @@ -55,6 +60,7 @@ jobs:

deploy:
name: Deploy documentation
if: github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
environment:
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,11 @@ jobs:
NPM_CONFIG_ACCESS: public
NPM_CONFIG_PROVENANCE: true

# Docs-only merges deploy after validation without requiring npm publication.
deploy-docs:
name: Deploy published documentation
if: needs.publish.result == 'success'
needs: publish
name: Deploy documentation
if: github.ref == 'refs/heads/main' && needs.validation.result == 'success'
needs: validation
permissions:
contents: read
id-token: write
Expand Down
169 changes: 95 additions & 74 deletions .intent/review-state.json

Large diffs are not rendered by default.

42 changes: 30 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,33 @@ match it. Use TypeScript 5.9 or newer with `strict: true`, ESM, and ES2022 suppo

## RPC

Share this contract with your server and acquire a matching **flat RPC client** before calling
`readUser`. Keep its Scope alive while the client is in use. The
Import the shared contract from both your server and client. Acquire a matching **flat RPC client**
before calling `readUser`, and keep its Scope alive while the client is in use. The
[RPC quick start](https://ueberbrot.github.io/effect-api-query/getting-started/quick-start/)
shows acquisition and cleanup.

**Shared contract — `rpc-contract.ts`**

```ts
import { QueryClient } from '@tanstack/query-core'
import { Schema } from 'effect'
import { createRpcQueryUtils } from 'effect-api-query'
import { Rpc, RpcGroup } from 'effect/rpc'
import type { RpcClient } from 'effect/rpc'

const usersRpc = RpcGroup.make(
export const usersRpc = RpcGroup.make(
Rpc.make('users.get', {
payload: { id: Schema.Int },
success: Schema.Struct({ id: Schema.Int, name: Schema.String }),
}),
)
```

**Client integration — `client.ts`**

```ts
import { QueryClient } from '@tanstack/query-core'
import { createRpcQueryUtils } from 'effect-api-query'
import type { RpcClient, RpcGroup } from 'effect/rpc'

import { usersRpc } from './rpc-contract.ts'

async function readUser(
client: RpcClient.RpcClient.Flat<RpcGroup.Rpcs<typeof usersRpc>>,
Expand All @@ -53,26 +62,35 @@ payload Schema's constructor input.

## HTTP

Share this HttpApi with your server and acquire its **ready HttpApiClient** before calling
`readUser`. Keep its transport resources alive while the client is in use. The
Import the shared HttpApi from both your server and client. Acquire its **ready HttpApiClient**
before calling `readUser`, and keep its transport resources alive while the client is in use. The
[HTTP quick start](https://ueberbrot.github.io/effect-api-query/getting-started/http-quick-start/)
shows acquisition and cleanup.

**Shared contract — `http-contract.ts`**

```ts
import { QueryClient } from '@tanstack/query-core'
import { Schema } from 'effect'
import { createHttpApiQueryUtils } from 'effect-api-query'
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from 'effect/http-api'
import type { HttpApiClient } from 'effect/http-api'

const usersApi = HttpApi.make('users-api').add(
export const usersApi = HttpApi.make('users-api').add(
HttpApiGroup.make('users').add(
HttpApiEndpoint.get('get', '/users/:id', {
params: { id: Schema.Int },
success: Schema.Struct({ id: Schema.Int, name: Schema.String }),
}),
),
)
```

**Client integration — `client.ts`**

```ts
import { QueryClient } from '@tanstack/query-core'
import { createHttpApiQueryUtils } from 'effect-api-query'
import type { HttpApiClient } from 'effect/http-api'

import { usersApi } from './http-contract.ts'

async function readUser(client: HttpApiClient.ForApi<typeof usersApi>, queryClient: QueryClient) {
const http = createHttpApiQueryUtils(usersApi, { client, keyPrefix: ['users-app'] })
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/astro.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default defineConfig({
starlightThemeBlack({
navLinks: [
{
label: `v${packageManifest.version}`,
label: 'npm',
link: packageUrl,
},
],
Expand Down
35 changes: 25 additions & 10 deletions apps/docs/src/content/docs/getting-started/http-quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,18 @@ First [install the package](/effect-api-query/getting-started/installation/). Th
serve the same `usersApi` contract at `http://localhost:3000`. Share the contract between your
server and browser application.

## Acquire the client and execute requests
## Define the shared contract

Place the declaration in `http-contract.ts`. Both the server and client import this module.

This complete module defines the API, acquires the ready client through a ManagedRuntime,
reads a user, writes a user, and releases its resources. Replace the base URL with your server.
**Shared contract — `http-contract.ts`**

```ts
import { MutationObserver, QueryClient } from '@tanstack/query-core'
import { Effect, ManagedRuntime, Schema } from 'effect'
import { createHttpApiQueryUtils } from 'effect-api-query'
import { FetchHttpClient } from 'effect/http'
import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from 'effect/http-api'
import { Effect, Schema } from 'effect'
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from 'effect/http-api'

const User = Schema.Struct({ id: Schema.Int, name: Schema.String })
const usersApi = HttpApi.make('users-api').add(
export const usersApi = HttpApi.make('users-api').add(
HttpApiGroup.make('users').add(
HttpApiEndpoint.get('get', '/users/:id', {
params: { id: Schema.Int },
Expand All @@ -35,6 +33,23 @@ const usersApi = HttpApi.make('users-api').add(
}),
),
)
```

## Acquire the client and execute requests

This client module acquires the ready client through a ManagedRuntime, reads a user, writes a
user, and releases its resources. Replace the base URL with your server.

**Client integration — `client.ts`**

```ts
import { MutationObserver, QueryClient } from '@tanstack/query-core'
import { ManagedRuntime } from 'effect'
import { createHttpApiQueryUtils } from 'effect-api-query'
import { FetchHttpClient } from 'effect/http'
import { HttpApiClient } from 'effect/http-api'

import { usersApi } from './http-contract.ts'

const runtime = ManagedRuntime.make(FetchHttpClient.layer)
const queryClient = new QueryClient()
Expand Down Expand Up @@ -83,7 +98,7 @@ See [HTTP Factory](/effect-api-query/reference/http-factory/).

## Keep a browser client alive

The module above awaits its mutation before disposal. For components, acquire once before
The client module above awaits its mutation before disposal. For components, acquire once before
mounting and keep the client alive for the application lifetime. Follow
[React client setup](/effect-api-query/guides/react-query/#acquire-and-mount-the-client) for a complete provider,
component, and cleanup callback.
Expand Down
35 changes: 25 additions & 10 deletions apps/docs/src/content/docs/getting-started/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,18 @@ First [install the package](/effect-api-query/getting-started/installation/). Th
serve the same `usersRpc` contract at `http://localhost:3000/rpc` with Effect's JSON RPC protocol.
Share the contract between your server and browser application.

## Acquire the client and execute requests
## Define the shared contract

Place the declaration in `rpc-contract.ts`. Both the server and client import this module.

This complete module defines the contract, acquires a ready flat client, reads a user, writes a
user, and releases its resources. Replace the URL with your server's RPC endpoint.
**Shared contract — `rpc-contract.ts`**

```ts
import { MutationObserver, QueryClient } from '@tanstack/query-core'
import { Effect, Exit, Layer, Schema, Scope } from 'effect'
import { createRpcQueryUtils } from 'effect-api-query'
import { FetchHttpClient } from 'effect/http'
import { Rpc, RpcClient, RpcGroup, RpcSerialization } from 'effect/rpc'
import { Effect, Schema } from 'effect'
import { Rpc, RpcGroup } from 'effect/rpc'

const User = Schema.Struct({ id: Schema.Int, name: Schema.String })
const usersRpc = RpcGroup.make(
export const usersRpc = RpcGroup.make(
Rpc.make('users.get', {
payload: {
id: Schema.Int,
Expand All @@ -33,6 +31,23 @@ const usersRpc = RpcGroup.make(
success: User,
}),
)
```

## Acquire the client and execute requests

This client module acquires a ready flat client, reads a user, writes a user, and releases its
resources. Replace the URL with your server's RPC endpoint.

**Client integration — `client.ts`**

```ts
import { MutationObserver, QueryClient } from '@tanstack/query-core'
import { Effect, Exit, Layer, Scope } from 'effect'
import { createRpcQueryUtils } from 'effect-api-query'
import { FetchHttpClient } from 'effect/http'
import { RpcClient, RpcSerialization } from 'effect/rpc'

import { usersRpc } from './rpc-contract.ts'

const protocol = RpcClient.layerProtocolHttp({ url: 'http://localhost:3000/rpc' }).pipe(
Layer.provide(RpcSerialization.layerJson),
Expand Down Expand Up @@ -85,7 +100,7 @@ do not. RPC and HTTP keys have separate namespaces.

## Keep a browser client alive

The module above finishes each operation before disposing its resources. For an application,
The client module above finishes each operation before disposing its resources. For an application,
acquire once before mounting components and release after unmounting them. Use the
[React client setup](/effect-api-query/guides/react-query/#acquire-and-mount-the-client) for a provider, component,
and cleanup callback. Its HTTP client setup can be replaced with the RPC acquisition above.
Expand Down
61 changes: 43 additions & 18 deletions apps/docs/src/content/docs/guides/react-query.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,51 @@ capture plain input for that execution.

## Acquire and mount the client

Create a small client module and mount it once from your application entry point.
Keep the shared contract, client lifetime, and React UI in separate modules. Mount the application
once from its entry point.

This read-only example requires React, React DOM, `@tanstack/react-query`, Effect, and
`effect-api-query`. The server must implement the shared contract below: `GET /users/:id`
returns `{ id: number, name: string }`.

### Contract and ready client
### Shared contract

Place this code in `users-app.tsx`. Move `usersApi` to your shared contract module when connecting
it to a real server. `startUsersApplication` acquires the ready HTTP API client and keeps one runtime
and QueryClient alive until `dispose` runs.
Place `usersApi` in `http-contract.ts`. Both the server and client import this module.

```tsx
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'
import { ManagedRuntime, Schema } from 'effect'
import { createHttpApiQueryUtils } from 'effect-api-query'
import type { RunPromiseExit } from 'effect-api-query'
import { FetchHttpClient } from 'effect/http'
import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from 'effect/http-api'
import { createRoot } from 'react-dom/client'
**Shared contract — `http-contract.ts`**

const usersApi = HttpApi.make('users-api').add(
```ts
import { Schema } from 'effect'
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from 'effect/http-api'

export const usersApi = HttpApi.make('users-api').add(
HttpApiGroup.make('users').add(
HttpApiEndpoint.get('get', '/users/:id', {
params: { id: Schema.Int },
success: Schema.Struct({ id: Schema.Int, name: Schema.String }),
}),
),
)
```

### Client lifetime

In `client.ts`, `startUsersApplication` acquires the ready HTTP API client and keeps one runtime
and QueryClient alive until `dispose` runs. Export its application type for the UI module.

**Client integration — `client.ts`**

const startUsersApplication = async (baseUrl: string) => {
```ts
import { QueryClient } from '@tanstack/react-query'
import { ManagedRuntime } from 'effect'
import { createHttpApiQueryUtils } from 'effect-api-query'
import type { RunPromiseExit } from 'effect-api-query'
import { FetchHttpClient } from 'effect/http'
import { HttpApiClient } from 'effect/http-api'

import { usersApi } from './http-contract.ts'

export const startUsersApplication = async (baseUrl: string) => {
const runtime = ManagedRuntime.make(FetchHttpClient.layer)
const running = new Set<Promise<unknown>>()
const runPromiseExit: RunPromiseExit = (effect, options) => {
Expand Down Expand Up @@ -79,6 +94,8 @@ const startUsersApplication = async (baseUrl: string) => {
throw error
}
}

export type UsersApplication = Awaited<ReturnType<typeof startUsersApplication>>
```

The supplied runner forwards TanStack's abort options and tracks in-flight reads. After cancellation,
Expand All @@ -87,11 +104,17 @@ before React renders. Acquisition failures also dispose the runtime.

### Component and provider

Continue in the same module. The component receives the ready utility tree through props;
`QueryClientProvider` exposes the matching cache to React Query.
In `users-app.tsx`, import the client setup and its application type. The component receives the
ready utility tree through props; `QueryClientProvider` exposes the matching cache to React Query.

**React UI — `users-app.tsx`**

```tsx
type UsersApplication = Awaited<ReturnType<typeof startUsersApplication>>
import { QueryClientProvider, useQuery } from '@tanstack/react-query'
import { createRoot } from 'react-dom/client'

import { startUsersApplication } from './client.ts'
import type { UsersApplication } from './client.ts'

const User = ({ application }: { readonly application: UsersApplication }) => {
const user = useQuery(application.http.users.get.queryOptions({ input: { params: { id: 1 } } }))
Expand Down Expand Up @@ -124,6 +147,8 @@ export const mountUsersApplication = async (element: HTMLElement, baseUrl: strin
In `main.tsx`, mount into the root element supplied by your HTML page. Replace the base URL with
your server's URL.

**Application entry point — `main.tsx`**

```ts
import { mountUsersApplication } from './users-app.tsx'

Expand Down
Loading
Loading