-
-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: generate types from OpenAPI schemas (#33)
* Prepare pages for multiple examples * Generate types from OpenAPI schema Resolves #32 * Save the petstore schema locally * Tweak the openapi docs so paths are correct * docs: update split `Endpoint` type * docs: capitalize OpenAPI headlines * chore: unify playground styling * chore: format types * refactor: outsource open api types generation into own file * chore: formatting * chore: add `pathe` dependency * refactor: simplify `resolvePath` * refactor: remove `pathParams` default val * fix: add `pathParams` fallback back in * chore: lint * fix: Use more explicit type in RequestBody * fix: Add petstore url to example env * chore: Remove pointless cast * refactor: use `Record<string, any>` instead of `object` * fix: add default opts value back * refactor: simplify if statement --------- Co-authored-by: Johann Schopplich <[email protected]>
- Loading branch information
1 parent
7a8790d
commit 6876f40
Showing
19 changed files
with
2,021 additions
and
185 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
# OpenAPI Types | ||
|
||
If your API has an [OpenAPI](https://swagger.io/resources/open-api/) schema, Nuxt API Party can use it to generate types for you. These include path names, supported HTTP methods, request body, response body, query parameters, and headers. | ||
|
||
Usage of this feature requires [`openapi-typescript`](https://www.npmjs.com/package/openapi-typescript) to be installed. This library generates TypeScript definitions from your OpenAPI schema file. | ||
|
||
Install it before proceeding: | ||
|
||
::: code-group | ||
|
||
```bash [pnpm] | ||
pnpm add -D openapi-typescript | ||
``` | ||
|
||
```bash [yarn] | ||
yarn add -D openapi-typescript | ||
``` | ||
|
||
```bash [npm] | ||
npm install -D openapi-typescript | ||
``` | ||
|
||
::: | ||
|
||
## Schema Generation | ||
|
||
Some web frameworks can generate an OpenAPI schema for you based on your configured routes. Some examples include: | ||
|
||
- [NestJS](https://docs.nestjs.com/openapi/introduction) | ||
- [FastAPI](https://fastapi.tiangolo.com/) | ||
- [Django](https://www.django-rest-framework.org/api-guide/schemas/) | ||
- [ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/tutorials/web-api-help-pages-using-swagger) | ||
- [Spring](https://springdoc.org/) | ||
- [Utopia](https://docs.rs/utoipa/latest/utoipa/) | ||
|
||
If your framework doesn't directly support it, there may also be an additional library that does. | ||
|
||
::: info | ||
If your API or framework uses the older OpenAPI 2.0 (aka Swagger) specification, you will need to install `openapi-typescript@5`, which is the latest version that supports it. | ||
::: | ||
|
||
## Configuring the schema | ||
|
||
To take advantage of these type features, add the `schema` property to your endpoint config. It should be set to a file path or URL of the OpenAPI schema or an async function returning the parsed OpenAPI schema. The file can be in either JSON or YAML format. | ||
|
||
The following schema will be used for the code examples on this page. | ||
|
||
```yaml | ||
# `schemas/myApi.yaml` | ||
openapi: 3.0.0 | ||
info: | ||
title: My API | ||
version: 0.1.0 | ||
paths: | ||
/foo: | ||
get: | ||
operationId: getFoos | ||
responses: | ||
200: | ||
content: | ||
application/json: | ||
schema: | ||
type: array | ||
items: | ||
$ref: '#/components/schemas/Foo' | ||
post: | ||
operationId: createFoo | ||
requestBody: | ||
content: | ||
application/json: | ||
schema: | ||
$ref: '#/components/schemas/Foo' | ||
responses: | ||
200: | ||
content: | ||
application/json: | ||
schema: | ||
$ref: '#/components/schemas/Foo' | ||
/foo/{id}: | ||
get: | ||
operationId: getFoo | ||
parameters: | ||
- name: id | ||
in: path | ||
type: number | ||
responses: | ||
200: | ||
content: | ||
application/json: | ||
schema: | ||
$ref: '#/components/schemas/Foo' | ||
components: | ||
schemas: | ||
Foo: | ||
type: object | ||
items: | ||
id: | ||
type: number | ||
bar: | ||
type: string | ||
required: | ||
- bar | ||
``` | ||
Reference the schema file in your endpoint config: | ||
```ts | ||
// `nuxt.config.ts` | ||
export default defineNuxtConfig({ | ||
apiParty: { | ||
myApi: { | ||
url: process.env.MY_API_API_BASE_URL!, | ||
schema: './schemas/myApi.yaml', | ||
}, | ||
}, | ||
}) | ||
``` | ||
|
||
## Using the Types | ||
|
||
For most usages, no further intervention is needed. Nuxt API Party will use the types generated from this config to infer the correct types automatically when `$myApi` and `useMyApiData` is used. | ||
|
||
However, there may be a few things you may want to do now that you have type information. | ||
|
||
### Extract the Response Body Type | ||
|
||
You can get the request and response bodies directly from the exported `components` interface of the virtual module containing the types. | ||
|
||
Using the schema above: | ||
|
||
```ts | ||
import { components } from '#nuxt-api-party/myApi' | ||
|
||
// { id?: number; foo: string } | ||
type Foo = components['schemas']['Foo'] | ||
``` | ||
### Use OpenAPI Defined Path Parameters | ||
OpenAPI can define path parameters on some endpoints. They are declared as `/foo/{id}`. Unfortunately, the endpoint is not defined as `/foo/10`, so using that as the path will break type inference. | ||
To get around this, set an object of the parameters to the property `pathParams`. You can then use the declared path for type inference, and the type checker will ensure you provide all required path parameters. The parameters will be interpolated into the path before the request is made. | ||
```ts | ||
const data = await $myApi('foo/{id}', { | ||
pathParams: { | ||
id: 10 | ||
} | ||
}) | ||
``` | ||
|
||
::: warning | ||
Issues will **NOT** be reported at runtime by Nuxt API Party if the wrong parameters are used. The **incomplete** path will be sent to the backend **AS IS**. | ||
::: | ||
|
||
### Route Method Overloading | ||
|
||
Some routes may be overloaded with multiple HTTP methods. The typing supports this natively and chooses the type based on the `method` property. When the property is omitted, the typing is smart enough to know `GET` is the default. | ||
|
||
In the example schema, `GET /foo` will return a `Foo[]` array, but `POST /foo` will return a `Foo` object. | ||
|
||
```ts | ||
// resolved type: `{ id?: number; bar: string }[]` | ||
const result1 = await $myApi('foo') | ||
|
||
// resolved type: `{ id?: number; bar: string }` | ||
const result = await $myApi('foo', { | ||
method: 'POST', | ||
body: { | ||
bar: 'string' | ||
} | ||
}) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
JSON_PLACEHOLDER_BASE_URL=https://jsonplaceholder.typicode.com | ||
PET_STORE_BASE_URL=https://petstore3.swagger.io/api/v3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,101 +1,23 @@ | ||
<script setup lang="ts"> | ||
import type { FetchError } from 'ofetch' | ||
import type { JsonPlaceholderComment } from './types' | ||
const route = useRoute() | ||
// Intended for similar use cases as `useFetch` | ||
const { data, pending, error } = await useJsonPlaceholderData<JsonPlaceholderComment>( | ||
'comments', | ||
{ | ||
query: computed(() => ({ | ||
postId: `${route.query.postId || 1}`, | ||
})), | ||
onResponse({ response }) { | ||
if (process.server) | ||
return | ||
// eslint-disable-next-line no-console | ||
console.log(response._data) | ||
}, | ||
}, | ||
) | ||
// eslint-disable-next-line no-console | ||
watch(error, value => console.log(value)) | ||
async function incrementPostId() { | ||
await navigateTo({ | ||
query: { | ||
postId: `${Number(route.query.postId || 1) + 1}`, | ||
}, | ||
}) | ||
// eslint-disable-next-line no-console | ||
console.log('Post ID:', route.query.postId) | ||
} | ||
const formResponse = ref() | ||
// Intended for similar use cases as `$fetch` | ||
async function onSubmit() { | ||
try { | ||
formResponse.value = await $jsonPlaceholder('posts', { | ||
method: 'POST', | ||
body: { | ||
title: 'foo', | ||
body: 'bar', | ||
userId: 1, | ||
}, | ||
}) | ||
// eslint-disable-next-line no-console | ||
console.log('formResponse:', formResponse.value) | ||
} | ||
catch (e) { | ||
console.error('statusCode:', (e as FetchError).statusCode) | ||
console.error('statusMessage:', (e as FetchError).statusMessage) | ||
console.error('data:', (e as FetchError).data) | ||
} | ||
} | ||
</script> | ||
|
||
<template> | ||
<Head> | ||
<Title>nuxt-api-party</Title> | ||
<Link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@exampledev/[email protected]/new.min.css" /> | ||
</Head> | ||
|
||
<header> | ||
<h1>nuxt-api-party</h1> | ||
<NuxtLink to="/"> | ||
<h1>nuxt-api-party</h1> | ||
</NuxtLink> | ||
<p> | ||
Requests are proxied by a Nuxt server route and passed back to the client. The playground uses <a href="https://jsonplaceholder.typicode.com/">{JSON} Placeholder</a> | ||
as an example API. The dynamic composables <code>$jsonPlaceholder</code> and <code>useJsonPlaceholderData</code> are generated by the module. | ||
Requests are proxied by a Nuxt server route and passed back to the client. | ||
The playground uses <a href="https://jsonplaceholder.typicode.com/">{JSON} Placeholder</a> | ||
and <a href="https://petstore3.swagger.io/">Swagger Petstore</a> as example APIs. | ||
The dynamic composables <code>$jsonPlaceholder</code> and <code>useJsonPlaceholderData</code> | ||
are generated by the module. | ||
</p> | ||
</header> | ||
|
||
<main> | ||
<h2>$jsonPlaceholder</h2> | ||
<p>Responses are <strong>not</strong> cached by default.</p> | ||
<blockquote>(Imagine form fields here)</blockquote> | ||
<p> | ||
<button @click="onSubmit()"> | ||
Submit | ||
</button> | ||
</p> | ||
<pre v-if="formResponse">{{ JSON.stringify(formResponse, undefined, 2) }}</pre> | ||
<hr> | ||
|
||
<h2>useJsonPlaceholderData</h2> | ||
<p>Responses are cached by default.</p> | ||
<p> | ||
Status: | ||
<mark v-if="pending">pending</mark> | ||
<code v-else>fetched</code> | ||
</p> | ||
<p> | ||
<button @click="incrementPostId()"> | ||
Increment Post ID | ||
</button> | ||
</p> | ||
<pre>{{ JSON.stringify(data, undefined, 2) }}</pre> | ||
<NuxtPage /> | ||
</main> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.