Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/quiet-clients-effect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kubb/plugin-effect-httpapiclient": minor
---

Add Effect v4 `HttpApi` contract and `HttpApiClient` generation. The generated client preserves status and content-type schemas, decodes date-time values as `DateTime.Utc`, and provides typed static or dynamic security credentials.
5 changes: 5 additions & 0 deletions .changeset/warm-effects-generate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kubb/plugin-effect": minor
---

Add `@kubb/plugin-effect` for generating Effect v4 beta schemas and matching TypeScript types from OpenAPI. Date-time codecs decode wire strings and annotations into Effect `DateTime.Utc` values.
6 changes: 6 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@
'@kubb/plugin-zod':
- any: ['packages/plugin-zod/**', '!packages/plugin-zod/package.json']

'@kubb/plugin-effect':
- any: ['packages/plugin-effect/**', '!packages/plugin-effect/package.json']

'@kubb/plugin-effect-httpapiclient':
- any: ['packages/plugin-effect-httpapiclient/**', '!packages/plugin-effect-httpapiclient/package.json']

'unplugin-kubb':
- any: ['packages/unplugin-kubb/**', '!packages/unplugin-kubb/package.json']

Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

**Official and community plugins for [Kubb](https://kubb.dev).**

This monorepo is home to official and community plugins for [Kubb](https://kubb.dev), the meta framework for code generation. Point Kubb at your OpenAPI specification and it generates TypeScript types, API clients, Zod schemas, React/Vue/Svelte/Solid Query hooks, Faker mocks, MSW handlers, and more.
This monorepo is home to official and community plugins for [Kubb](https://kubb.dev), the meta framework for code generation. Point Kubb at your OpenAPI specification and it generates TypeScript types, API clients, Effect and Zod schemas, React/Vue/Svelte/Solid Query hooks, Faker mocks, MSW handlers, and more.

Want to build your own plugin? See [CONTRIBUTING.md](./CONTRIBUTING.md).

Expand All @@ -41,6 +41,7 @@ Maintained by the Kubb team. Kubb v5 OpenAPI configs use [`@kubb/adapter-oas`](h
| Package | Version | Description |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| [`@kubb/plugin-axios`](./packages/plugin-axios) | [![npm version](https://img.shields.io/npm/v/@kubb/plugin-axios.svg)](https://npmx.dev/package/@kubb/plugin-axios) | Type-safe HTTP client based on [Axios](https://github.com/axios/axios) |
| [`@kubb/plugin-effect-httpapiclient`](./packages/plugin-effect-httpapiclient) | [![npm version](https://img.shields.io/npm/v/@kubb/plugin-effect-httpapiclient.svg)](https://npmx.dev/package/@kubb/plugin-effect-httpapiclient) | [Effect](https://github.com/Effect-TS/effect-smol) v4 HttpApiClient generation |
| [`@kubb/plugin-fetch`](./packages/plugin-fetch) | [![npm version](https://img.shields.io/npm/v/@kubb/plugin-fetch.svg)](https://npmx.dev/package/@kubb/plugin-fetch) | Type-safe HTTP client based on the Fetch API |

### Zod
Expand All @@ -49,6 +50,12 @@ Maintained by the Kubb team. Kubb v5 OpenAPI configs use [`@kubb/adapter-oas`](h
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| [`@kubb/plugin-zod`](./packages/plugin-zod) | [![npm version](https://img.shields.io/npm/v/@kubb/plugin-zod.svg)](https://npmx.dev/package/@kubb/plugin-zod) | [Zod](https://github.com/colinhacks/zod) schema generation for runtime validation |

### Effect

| Package | Version | Description |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| [`@kubb/plugin-effect`](./packages/plugin-effect) | [![npm version](https://img.shields.io/npm/v/@kubb/plugin-effect.svg)](https://npmx.dev/package/@kubb/plugin-effect) | [Effect](https://github.com/Effect-TS/effect-smol) v4 schema and TypeScript generation |

### Data fetching

| Package | Version | Description |
Expand Down Expand Up @@ -85,6 +92,7 @@ Plugins built and maintained by the community. Want to add yours? See [CONTRIBUT
| [`client`](./examples/client) | Generate API clients with Axios |
| [`fetch`](./examples/fetch) | Generate API clients with Fetch |
| [`zod`](./examples/zod) | Generate Zod validation schemas |
| [`effect`](./examples/effect) | Generate Effect v4 schemas |
| [`react-query`](./examples/react-query) | Generate React Query hooks |
| [`vue-query`](./examples/vue-query) | Generate Vue Query composables |
| [`faker`](./examples/faker) | Generate Faker.js mock data |
Expand Down
21 changes: 21 additions & 0 deletions examples/effect-httpapiclient/kubb.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { adapterOas } from '@kubb/adapter-oas'
import { pluginEffect } from '@kubb/plugin-effect'
import { pluginEffectHttpApiClient } from '@kubb/plugin-effect-httpapiclient'
import { defineConfig } from 'kubb/config'

export default defineConfig({
root: '.',
input: './petStore.yaml',
adapter: adapterOas({ unknownType: 'unknown', dateType: 'date' }),
output: {
path: './src/gen',
clean: true,
},
plugins: [
pluginEffect({ output: { path: 'effect', barrel: { type: 'named' } } }),
pluginEffectHttpApiClient({
output: { path: 'effectHttpApiClient', barrel: { type: 'named' } },
baseURL: 'https://petstore.swagger.io/v2',
}),
],
})
33 changes: 33 additions & 0 deletions examples/effect-httpapiclient/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "effect-httpapiclient-pet-store",
"version": "0.0.0",
"private": true,
"description": "Effect v4 HttpApiClient PetStore example",
"license": "MIT",
"author": "stijnvanhulle",
"repository": {
"type": "git",
"url": "https://github.com/kubb-labs/plugins.git",
"directory": "examples/effect-httpapiclient"
},
"type": "module",
"sideEffects": false,
"scripts": {
"generate": "KUBB_DISABLE_TELEMETRY=1 kubb --config kubb.config.js",
"test": "vitest --passWithNoTests",
"typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
},
"dependencies": {
"@kubb/adapter-oas": "catalog:",
"@kubb/plugin-effect": "workspace:*",
"@kubb/plugin-effect-httpapiclient": "workspace:*",
"effect": "catalog:",
"kubb": "catalog:",
"typescript": "catalog:"
},
"engines": {
"node": ">=22",
"pnpm": ">=11.0.0"
},
"packageManager": "pnpm@11.5.0"
}
159 changes: 159 additions & 0 deletions examples/effect-httpapiclient/petStore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
openapi: 3.0.3
info:
title: Effect HttpApiClient PetStore
version: 1.0.0
paths:
/pets/{pet_id}:
get:
operationId: getPet
summary: Get one pet
tags:
- pet
parameters:
- name: pet_id
in: path
required: true
schema:
type: integer
format: int64
- name: fields
in: query
style: pipeDelimited
explode: false
schema:
type: array
items:
type: string
- name: X-Trace
in: header
schema:
type: string
- name: session_id
in: cookie
style: form
explode: false
schema:
type: string
security:
- api_key: []
- petstore_auth:
- read:pets
responses:
'200':
description: Pet found
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
'404':
description: Pet not found
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
/pets:
get:
operationId: listPets
summary: List pets
tags:
- pet
parameters:
- name: status
in: query
style: form
explode: true
schema:
type: array
items:
type: string
enum:
- available
- pending
security:
- petstore_auth:
- read:pets
- list:pets
responses:
'200':
description: Pet list
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
/orders:
post:
operationId: placeOrder
summary: Place an order
tags:
- store
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OrderInput'
responses:
'200':
description: Created order
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
components:
schemas:
Pet:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
updatedAt:
type: string
format: date-time
OrderInput:
type: object
required:
- id
properties:
id:
type: integer
format: int64
Order:
type: object
required:
- id
properties:
id:
type: integer
format: int64
shipDate:
type: string
format: date-time
ApiError:
type: object
required:
- message
properties:
message:
type: string
securitySchemes:
api_key:
type: apiKey
name: x-api-key
in: header
petstore_auth:
type: oauth2
flows:
implicit:
authorizationUrl: https://example.com/oauth/authorize
scopes:
read:pets: Read pets
list:pets: List pets
114 changes: 114 additions & 0 deletions examples/effect-httpapiclient/src/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import * as DateTime from 'effect/DateTime'
import { Effect, Redacted } from 'effect'
import { HttpClient, HttpClientResponse } from 'effect/unstable/http'
import { describe, expect, test } from 'vitest'
import { ApiClient, makeSecurityLayer, type SecurityCredentialRequest, type SecurityCredential } from './gen/effectHttpApiClient/index.ts'

type CapturedRequest = {
method: string
url: string
apiKey: string | undefined
cookie: string | undefined
}

function makeMockClient({ body, captured }: { body: unknown; captured: Array<CapturedRequest> }): HttpClient.HttpClient {
return HttpClient.make((request, url) => {
captured.push({ method: request.method, url: url.toString(), apiKey: request.headers['x-api-key'], cookie: request.headers.cookie })
return Effect.succeed(
HttpClientResponse.fromWeb(
request,
new Response(JSON.stringify(body), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
),
)
})
}

describe('generated Effect HttpApiClient', () => {
test('encodes path parameters and injects a static API key', async () => {
const captured: Array<CapturedRequest> = []
const httpClient = makeMockClient({ body: { id: '10', name: 'Milo', photoUrls: [] }, captured })
const program = Effect.gen(function* () {
const client = yield* ApiClient
return yield* client.pet.getPet({
params: { petId: 10n },
query: { fields: ['id', 'name'] },
headers: { cookies: { sessionId: 'session' } },
})
}).pipe(
Effect.provide(
makeSecurityLayer({
credentials: {
api_key: { _tag: 'ApiKey', value: Redacted.make('secret') },
},
}),
),
Effect.provideService(HttpClient.HttpClient, httpClient),
)

await expect(Effect.runPromise(program)).resolves.toMatchObject({ id: 10n, name: 'Milo' })
expect(captured).toStrictEqual([
{
method: 'GET',
url: 'https://petstore.swagger.io/v2/pets/10?fields=id%7Cname',
apiKey: 'secret',
cookie: 'session_id=session',
},
])
})

test('passes endpoint scopes to a dynamic credential resolver', async () => {
const captured: Array<CapturedRequest> = []
const requests: Array<SecurityCredentialRequest> = []
const httpClient = makeMockClient({ body: [], captured })
const resolve = (request: SecurityCredentialRequest): Effect.Effect<SecurityCredential | undefined> => {
requests.push(request)
return Effect.succeed({ _tag: 'Bearer', token: Redacted.make('token') })
}
const program = Effect.gen(function* () {
const client = yield* ApiClient
return yield* client.pet.listPets({ query: { status: ['available', 'pending'] } })
}).pipe(Effect.provide(makeSecurityLayer({ resolve })), Effect.provideService(HttpClient.HttpClient, httpClient))

await Effect.runPromise(program)
expect(requests).toStrictEqual([
{
endpoint: 'listPets',
scheme: 'petstore_auth',
scopes: ['read:pets', 'list:pets'],
},
])
expect(captured[0]?.url).toBe('https://petstore.swagger.io/v2/pets?status=available&status=pending')
})

test('fails before transport when no security alternative is complete', async () => {
const captured: Array<CapturedRequest> = []
const httpClient = makeMockClient({ body: {}, captured })
const program = Effect.gen(function* () {
const client = yield* ApiClient
return yield* client.pet.getPet({ params: { petId: 10n }, query: {}, headers: { cookies: {} } })
}).pipe(Effect.provide(makeSecurityLayer()), Effect.provideService(HttpClient.HttpClient, httpClient), Effect.flip)

await expect(Effect.runPromise(program)).resolves.toMatchObject({
_tag: 'MissingSecurityCredentials',
endpoint: 'getPet',
requirements: [['api_key'], ['petstore_auth']],
})
expect(captured).toStrictEqual([])
})

test('decodes date-time responses as DateTime.Utc', async () => {
const captured: Array<CapturedRequest> = []
const httpClient = makeMockClient({ body: { id: '10', shipDate: '2026-07-14T10:30:00.000Z' }, captured })
const program = Effect.gen(function* () {
const client = yield* ApiClient
return yield* client.store.placeOrder({ payload: { id: 10n } })
}).pipe(Effect.provide(makeSecurityLayer()), Effect.provideService(HttpClient.HttpClient, httpClient))

const order = await Effect.runPromise(program)
expect(order.shipDate && DateTime.isUtc(order.shipDate)).toBe(true)
expect(order.shipDate && DateTime.formatIso(order.shipDate)).toBe('2026-07-14T10:30:00.000Z')
})
})
Loading