Generated TypeScript client
The generated TypeScript client provides you with an end-to-end, type-safe client library which you can use to easily authenticate and query your Keel APIs for frontend projects. It has the following primary features:
- A type-safe SDK of your Keel API
- Support for Password, ID Token and Single Sign-On authentication
- Automatic refreshing of expiring access token
Requires version 0.380.0 or higher of the Keel CLI.
Generating the client
To generate the client, use the client
command in the CLI from within a Keel project
keel client
This will generate a single file with zero dependancies in your current directory.
To output the client directly into your frontend project you can either pass an output path.
keel client --output ../myFrontend/src
Or by running the CLI from your frontend project and referencing the Keel directory
keel client -d ../myKeelApp
This will generate a client for the first API in your project. If you have multiple APIs you can specify which API to generate for with the -a apiName
flag
This client uses fetch so will only work in environments where fetch is available
Using the client
First, import APIClient
from the generated file and create a new instance with the deployed url of your project (or the localhost (opens in a new tab) address if running locally)
This baseUrl
url should include the API name path but not the protocol (e.g. json/rpc/gql). For a local environment with keel run
, this will be http://localhost:8000/api
.
import { APIClient } from "../keelClient";
const keel = new APIClient({
baseUrl: process.env.API_BASE_URL, // http://localhost:8000/api
});
Navigating the Generated client
The generated client is segmented into three parts; api
, auth
, and client
.
api
This includes all the actions that you have specified in your Keel schema. For example, if you have an action that lists all users, here is how you can use the generated client to execute this action on the client-side:
const keel = new APIClient({
baseUrl: process.env.API_BASE_URL, // http://localhost:8000/api
});
// use generated client to execute the action on the client-side
const allUsers = await keel.api.queries.listUsers()
// log response from API to console
console.log(response.data?.results)
You can also use .api.mutations
if you have actions that mutate data. For example, if you have an action that deletes a user, you can use it like so:
const keel = new APIClient({
baseUrl: process.env.API_BASE_URL, // http://localhost:8000/api
});
// use generated client to execute the action on the client-side
const response = await keel.api.mutations.createUser({ name: name, email: email});
// log response from API to console
console.log(response.data)
auth
Here you are able to authenticate identities with any of our supported authentication flows.
providers()
: Returns the currently configured 3rd party authentication providers and their Single Sign-On login URLs.authenticateWithPassword()
: Authenticate with the password flow.authenticateWithIdToken()
: Authenticate with an ID Token.authenticateWithSingleSignOn()
: Authenticate with an authorization code acquired from Single Sign-On authentication.isAuthenticated()
: Returns true if the session has not expired. If expired, it will attempt to refresh the session from the authentication server.refresh()
: Forcefully refreshes the session with the authentication server.logout()
: Logs out the session on the client and revokes the refresh token with the authentication server.
client
These are the core client actions that serve as configuration options for the client. They remain consistent across every Keel schema. They include:
setHeaders()
setHeader()
setBaseUrl()
Authenticating
Authentication is simple with the generated client because access tokens and refresh tokens are handled implicitly. Making authorized calls to your API happens under the hood. The client will also automatically refresh expiring access tokens.
Once authenticated, the client stores the active session in memory. This means that when a new instance of the client is created the session will be lost and authentication will need to take place again. However, you can implement your own token persistence in order to retain authentication over a longer period and between browser sessions.
Password authentication
First, let's demonstrate how to authenticate the client with an email and password.
const keel = new APIClient({
baseUrl: process.env.API_BASE_URL,
});
// authenticate the client with email and password
const response = await keel.auth.authenticateWithPassword({ email: email, password: pass });
if (response.error) {
// an error occurred
}
// check that the client is still authenticated
const isAuthenticatedResponse = await keel.auth.isAuthenticated();
if (response.data) {
// the client is still authenticated
}
// perform an authenticated request to your Keel API
const createPost = await keel.api.mutations.createBlogPost({ title: title });
ID Token authentication
With ID Token authentication, you would have already configured the 3rd-party authentication provider correctly in your keelconfig.yaml
and you would have performed the necessary authentication flow with that provider in order to acquire an ID Token. Once you have this ID Token, you can authenticate as shown below.
// authenticate the client using an ID token from a 3rd-party provider
const response = await keel.auth.authenticateWithIdToken({ idToken: idToken });
Single Sign-On authentication
With Single Sign-On authentication, you would have started the authentication flow by redirecting the user's browser to the authorizeUrl
URL found when calling await keel.auth.providers()
. When handling the callback (the redirectUrl
as configured in your keelconfig.yaml
), you can then use the authorization code received to authenticate the client.
// authenticate the client using an authorization code from Single Sign-On
const response = await keel.auth.authenticateWithSingleSignOn({ code: authCode });
Sign in and sign up
Both the authenticateWithPassword()
and authenticateWithIdToken()
functions also accept the optional argument 'createIfNotExists'. Setting this to false
will not create a new identity if it doesn't exist - i.e. a sign in flow.
The response from a successful authenticateWithPassword()
, authenticateWithIdToken()
, or authenticateWithSingleSignOn()
includes the field identityCreated
. This will be true
when the identity was created as a result of authentication - i.e. the identity has signed up. See example below.
const response = await keel.auth.authenticateWithSingleSignOn({ code: authCode });
if (response.error) {
// an error occurred
}
if (response.data.identityCreated) {
// a new identity was created
}
Token refresh and expiry
The client will automatically refresh your access token until such a point that the refresh token has expired. This means that you do not need to concern yourself with refreshing at all. It also supports Refresh Token Rotation, if enabled, which we highly recommend.
However, if the session has expired (i.e. the refresh token has expired), you will be required to reauthenticate the identity to start a new session.
const response = await keel.api.mutations.createBlogPost({ title: title });
if (response.error && response.error.type == "forbidden") {
request.redirect(302, "/login");
return;
}
Session persistence
By default the client will keep both the access and refresh tokens in memory, which although is ok for browser usage is not appropriate for server-side usage for example with frameworks like Next.js or Remix. Also it means that if the page is refreshed the user will need to reauthenticate.
To make it possible to persist auth tokens correctly in different environments or frameworks the client allows you to set custom token stores for both the access and refresh tokens. The interface for a token store is:
interface TokenStore {
// Set a token in the store
set(token: string | null): void;
// Retrieve a token from the store
get(): string | null;
}
Here are some examples of how you can implement a token store.
In the browser we can use the Local Storage API to persist the tokens. The client can also be created just once globally and used all throughout your app.
import { APIClient, TokenStore } from "./keelClient.ts"; // the generated client
class LocalStorageStore implements TokenStore {
#key: string;
constructor(key: string) {
this.#key = key;
}
get() {
return localStorage.get(this.#key);
}
set(token: string | null): void {
if (token) {
localStorage.set(this.#key, token);
} else {
localStorage.removeItem(this.#key);
}
}
}
const client = new APIClient({
// or if using Next.js something like process.env.NEXT_PUBLIC_BASE_URL
baseUrl: meta.import.env.VITE_KEEL_BASE_URL,
// token stores
accessTokenStore: new LocalStorageStore("keel-access-token"),
refreshTokenStore: new LocalStorageStore("keel-refresh-token"),
});
import { client } from "~/keel/index";
export function MyComponent() {
// use the client
}
Error handling
The response of an action is a Promise
that resolves to an object that either contains the data
or an error
object.
type Data<T> = {
data: T;
error?: never;
};
type Error = {
data?: never;
error: ErrorObj ;
};
type ErrorObj = {
type: "forbidden" | "not_found" | "internal_server_error" | "unknown" | "unauthorized" | "bad_request";
message: string;
requestId?: string; // can be used to access traces in the console
};
You can then check the presence of the error
object to handle any errors
const myAction = await keel.myAction({
input: "foo"
});
if (myAction.error) {
console.log("Failed", myAction.error.type);
return;
}
const { data } = myAction
// Do things with the data