@jsheaven/astro-client-generator
Generates TypeScript API client code for your Astro endpoints. No manual
fetch()
code writing anymore.
User Stories
-
As an Astro developer, I don't want to write the
fetch()
code for my Astro endpoints manually -
As an Astro developer, I want type-safety and auto-complete in my IDE when calling my Astro endpoints.
See: LIVE DEMO
Features
- ✅ Generates TypeScript/JavaScript API clients for Astro endpoints (
get
,post
,del
,all
, etc.) - ✅ Available as a simple Astro integration, API and simple to use CLI (for testing and npm scripts)
- ✅ Uses
fetch
under the hood, therefore works isomorphic in browser and in SSR/SSG - ✅ Supports JWT/token based authentication using Cookies and
Authorization
header - ✅ Comes with two parsers:
naive
(highly optimized) andbaseline
(deoptimization, safer) - ✅ Just
2.26 kb
nano sized library - ✅
0 byte
runtime overhead/dependencies as it just generates vanilla TS/JS code - ✅ Tree-shakable and side-effect free
- ✅ Runs on Windows, Mac, Linux, CI tested
- ✅ First class TypeScript support
- ✅ 100% Unit Test coverage
See the TODO list example. Example of the generated API client code.
Example usage / test it (CLI)
If you want to test it, simply run this command in your Astro project root folder:
npx @jsheaven/astro-client-generator generate
You need at least version 18 of Node.js installed.
Use the Astro intergration
- yarn:
yarn add @jsheaven/astro-client-generator
- npm:
npm install @jsheaven/astro-client-generator
Setup
yarn add @jsheaven/astro-client-generator
npm install @jsheaven/astro-client-generator
Integrate into Astro
Add the following code to your astro.config.(js|ts)
:
import { apiClientGenerator } from '@jsheaven/astro-client-generator'
// https://astro.build/config
export default defineConfig({
...
integrations: [
// generate client API code for the endpoints
apiClientGenerator(),
]
})
You can enhance and customize this configuration:
import { apiClientGenerator } from '@jsheaven/astro-client-generator'
// https://astro.build/config
export default defineConfig({
...
integrations: [
apiClientGenerator({
// == all settings displayed here are optional. What you see here, are default values ==
/** site URL to request from */
site: 'http://localhost:3000',
/** folder to the API directory on disk (source code) */
apiDir: './src/pages/api',
/** API base URL for calling the API (only relevant if you host in a subdir, it's unlikely) */
baseUrl: '',
/** folder on disk to write the client code to */
outDir: './src/pages/api-client',
/** parser to use. 'naive' comes with constraints (non-standard-compliant), 'baseline' is 900x slower */
parser: 'naive',
/** path to your tsconfig.json, from the root folder of your project */
tsConfigPath: './tsconfig.json',
}),
]
})
Lifecycle
It's important to note that you should run astro build
(aka. npm run build
/yarn build
)
whenever you change the interface definitions, imported models or the name of your endpoint
implementations. This will make sure, that the client code is re-generated and stays in sync.
How to use the generated clients?
Simple example of a `get` endpoint without `ApiRequest`
src/pages/api/user.ts
import { APIRoute } from 'astro'
import { User } from '../../model/User'
import { readFile } from 'fs/promises'
export interface ApiResponse {
error?: string
user: Array<Todo>
}
// impl. of a simple GET endpoint -- that's why the client method will be called: userClientGet
// in a file called user-client.ts
export const get: APIRoute = async ({ params, request, url }) => {
return new Response(
JSON.stringify({
user: {
id: 1,
firstName: 'Aron',
email: 'foo@bar.domain',
},
} as ApiResponse),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
},
)
}
Making requests using the generated API client
It's super trivial - yet very powerful, as you hardly can go wrong because
of the strong typing. Forget about any complexity in using fetch()
, and
forget about debugging for typos.
You can run requests from the server-side (SSR, SSG):
src/components/Header.astro
import { getUser } from '../pages/client-api/user-client'
// let's say we're using Cookie authentication (@jsheaven/astro-auth),
// or no authentication -- so we don't have to do anything
const user = await getUser()
But feel free to use the same generated client code in any frontend framework or even vanilla JS/TS.
To learn something new, let's use some extra parameters here -- in case we use an Authentication: Bearer $jwt
token authentication maybe:
src/components/react/Header.tsx
import { getUser } from '../pages/client-api/user-client'
import { getToken } from '@jsheaven/astro-oauth'
// because the user GET endpoint has no body, the first argument is the fetch() options
const user = await getUser({
headers: {
Authentication: `Bearer ${getToken()}`,
},
})
CommonJS
const { generateClientApis } = require('@jsheaven/astro-client-generator')
// same API like ESM variant
Naive parser pros & cons
The naive
parser in this package is a hand-written, highly optimized parser that is
capable of handling a subset of the official TypeScript / JavaScript syntax.
In naive
parsing mode, code generation will happen in 2 to 5ms
per endpoint,
file in baseline
mode, you can espect at least 1 sec
per endpoint. This is,
why naive
parsing is the default configuration.
If you follow the following rules, the naive
parser should be just working fine:
- write
import
statements in a single line - write
type
definitions in a single line - don't use
{
and}
inside of comments ininterface
definitions - endpoints must either be declared as async function expressions like
export const get = ( ... ) => { ... }
- or as an async function
async function get ( ... ) { ... }
- any endpoint method (
get
,post
,del
, etc. that Astro supports, is supported), but keep the method name lowercase
However, if you're running into errors, please use the baseline
parser:
import { apiClientGenerator } from '@jsheaven/astro-client-generator'
// https://astro.build/config
export default defineConfig({
...
integrations: [
apiClientGenerator({
...
parser: 'baseline',
...
}),
]
})
Please take a look at the test fixtures to get an idea about what syntax is definitely supported. It should cover most of the simple and advanced use-cases.
Roadmap
At the moment, generating client APIs for dynamic endpoints (such as [...dynamic]
and [foo]
, [bar]
) isn't supported. However, this is a feature that can be implemented, but I'd only see a good
reason to do that if the community demand is substantial.
Why no initial support for dynamic routing client API generation?
In traditional APIs, it's common sense / easier to transport all parameters for an endpoint
using a single source of truth - meaning: The request body. Because of that, it is wise to simply
define a ApiRequest
interface for carrying the JSON data from client to the endpoint and a
ApiResponse
interface for the way back, but to keep the route itself static.
If you're implementing dynamic endpoints, you'd usually use them to render response data in-place such as user-specific JSON data, user-specific or authentication-/guarded image rendering and such, following semantic routing for SEO purposes. As these routes are usually not used for traditional API use, and as you can, if you need the same functionality, also simply abstract the business logic out of the endpoint and expose it in a second, static API endpoint, I see no clear advantage for implementing the feature and making the implementation unncessarily complex. However, if the community disagrees at large, I'd definitely love to reiterate on that thought and decision.
Support for basic auth / request param based authentication
Because it is uncommon, insecure and a kinda dated concept to use basic auth, or request param based authentication methods for APIs, it is not covered by initial supported through this client generation library.
If you'd like to provide authentication credentials in a modern and secure way,
opt-in for OpenID authentication / OIDC flows (out-of-scope for the generator) and
then carry a JWT token to your endpoints. Naturally, you'd simply use a Cookie which
is automatically carried to the server as a HTTP header through the fetch
API that
is used in the client implementation.
You can also always use the built-in request customization feature to provide e.g.
the Authentication
header when using an API client that has been generated by
this library:
import { myApiClient } from '../pages/api-client/myApiClient'
import { getToken } from '@jsheaven/astro-oauth'
myApiClient(
/* request props/ POST body */ {
foo: 'bar',
},
/* optional, fetch() API option overrides */ {
headers: {
Authorization: `Bearer ${getToken()}`,
},
},
)