Generated Client

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.

keel/index.ts
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"),
});
components/MyComponent.tsx
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