Skip to content

oliver-oloughlin/jex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

84 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

jex

Release Score Tests License

jex is a configurable API client that lets you define strongly-typed HTTP calls using a JSON-like schema. Data types can be defined and validated using any third-party library of your choosing, such as Zod, or you can use the built-in schema helper which provides simple type casting. jex also provides a plugin API, enabling easy authentication, logging, request throttling and more.

Highlights

  • Strong typing with smart type inference
  • Flexible plugin API
  • Built-in plugins for authentication, request throttling and retrying
  • Support for logging

Installation

Install jex on your preferred platform:

deno add @olli/jex
npx jsr add @olli/jex
pnpm dlx jsr add @olli/jex
bunx jsr add @olli/jex

How to use

Create a basic API client

Using the built-in schema builder provides type inference only, and does not do any runtime validation of data.

import { jex, schema } from "@olli/jex"

type Data = {
  foo: string
  bar: number
}

const client = jex({
  baseUrl: "https://domain.com/api",
  endpoints: {
    "/foo": {
      get: {
        data: schema<Data>(),
      },
    },
  },
})

const result = await client["/foo"].get()
if (result.ok) {
  // Inferred as { foo: string, bar: number }
  const data = result.data
} else {
  // Inferred as null
  const data = result.data
}

With params, query and body

import { jex, schema } from "@olli/jex"

const client = jex({
  baseUrl: "https://domain.com/api",
  endpoints: {
    // Also supports notations: "/foo/[bar]", "/foo/{bar}", "/foo/<bar>"
    "/foo/:bar": {
      post: {
        body: schema<{ baz: boolean }>(),
        query: schema<{ q: string; n?: number }>(),
      },
    },
  },
})

const result = await client["/foo/:bar"].post({
  params: {
    bar: "Hello",
  },
  query: {
    q: "World",
  },
  body: {
    baz: true,
  },
})

if (result.ok) {
  // Inferred as null
  const data = result.data
}

Using Zod

Using a data validation library such as Zod provides both type inference and runtime validation of data.

import { jex } from "@olli/jex"
import { z } from "zod"

const DataSchema = z.object({
  foo: z.string(),
  bar: z.number(),
})

const client = jex({
  baseUrl: "https://domain.com/api",
  endpoints: {
    "/foo": {
      get: {
        data: DataSchema,
      },
    },
  },
})

const result = await client.foo.get()
if (result.ok) {
  // Inferred as { foo: string, bar: number }
  const data = result.data
} else {
  // Inferred as null
  const data = result.data
}

Plugins

jex also provides a handful of built-in plugins to provide easy logging, authentication and more. Plugins can be applied for all endpoints, all actions of a specific endpoint, or for a specific action of a specific endpoint.

import { jex } from "@olli/jex"
import { example } from "@olli/jex/example"

const client = jex({
  baseUrl: "https://domain.com/api",
  // Applied for all endpoints and actions
  plugins: [example()],
  endpoints: {
    "/foo": {
      // Applied for all actions of this endpoint
      plugins: [example()],
      get: {
        // Applied for this action only
        plugins: [example()],
      },
    },
  },
})

Logger

Provides basic logging of outgoing requests and incoming responses.

import { jex } from "@olli/jex"
import { logger } from "@olli/jex/logger"

// With default log function
const client = jex({
  baseUrl: "https://domain.com/api",
  plugins: [logger()],
  endpoints: {},
})

// With specified log function
const client = jex({
  baseUrl: "https://domain.com/api",
  plugins: [logger({ fn: console.info })],
  endpoints: {},
})

Basic Auth

Provides basic authentication by setting the Authorization header.

import { jex } from "@olli/jex"
import { basicAuth } from "@olli/jex/auth"

const client = jex({
  baseUrl: "https://domain.com/api",
  plugins: [basicAuth({
    username: "olli",
    password: "secret123",
  })],
  endpoints: {},
})

API Key Auth

Provides API key authentication using headers by default, or alternatively query.

import { jex } from "@olli/jex"
import { apiKeyAuth } from "@olli/jex/auth"

const client = jex({
  baseUrl: "https://domain.com/api",
  plugins: [apiKeyAuth({ apiKey: "secret_key" })],
  endpoints: {},
})
import { jex } from "@olli/jex"
import { apiKeyAuth } from "@olli/jex/auth"

const client = jex({
  baseUrl: "https://domain.com/api",
  plugins: [apiKeyAuth({
    apiKey: "secret_key",
    apiKeyName: "X-API-KEY", // default
    appId: "my-app",
    appIdName: "X-APP-ID", // default
    strategy: "query", // default = "headers"
  })],
  endpoints: {},
})

Bearer Auth

Provides bearer (token) authentication. Accepts both a static bearer token or config for fetching a token from a server endpoint.

import { jex } from "@olli/jex"
import { bearerAuth } from "@olli/jex/auth"

const client = jex({
  baseUrl: "https://domain.com/api",
  // Static token
  plugins: [bearerAuth({ token: "super_secret_token" })],
  endpoints: {},
})
import { jex, schema } from "@olli/jex"
import { bearerAuth } from "@olli/jex/auth"

const client = jex({
  baseUrl: "https://domain.com/api",
  // Dynamically fetched token using basic auth
  plugins: [bearerAuth({
    tokenUrl: "https://domain.com/api/token",
    tokenSchema: schema<{ token: string; expiresAt: number }>(),
    mapper: (data) => data.token,
    validator: (data) => data.expiresAt > Date.now(),
    credentials: {
      username: "olli",
      password: "banana123",
    },
  })],
  endpoints: {},
})

Retry List

Retries failed requests in a progressive manner, following the provided list of retry delays, specified in milliseconds.

import { jex } from "@olli/jex"
import { retryList } from "@olli/jex/retry"

const client = jex({
  baseUrl: "https://domain.com/api",
  // First waits 500ms, then 1000ms, and then 3000ms between retries.
  // Returns failed response if last attempt fails
  plugins: [retryList([500, 1000, 3000])],
  endpoints: {},
})

Fixed Throttle

Throttles requests based on a fixed interval, specified in milliseconds.

import { jex } from "@olli/jex"
import { fixedThrottle } from "@olli/jex/throttle"

const client = jex({
  baseUrl: "https://domain.com/api",
  // Ensures a minimum delay of 1 second between requests
  plugins: [fixedThrottle(1000)],
  endpoints: {},
})

Rate Throttle

Throttle requests based on a request rate limit.

import { jex } from "@olli/jex"
import { rateThrottle } from "@olli/jex/throttle"

const client = jex({
  baseUrl: "https://domain.com/api",
  // Throttles requests to 100 per minute
  plugins: [rateThrottle("100/min")],
  endpoints: {},
})

Default Init

Set default request options that will always be applied for the given plugin scope unless overridden.

import { jex } from "@olli/jex"
import { defaultInit } from "@olli/jex/init"

const client = jex({
  baseUrl: "https://domain.com/api",
  plugins: [defaultInit({
    headers: {
      "x-client-id": "my-app",
    },
  })],
  endpoints: {},
})

Development

Any contributions are welcomed and appreciated. How to contribute:

  • Clone this repository
  • Add feature / Refactor
  • Add or refactor tests as needed
  • Ensure code quality (check + lint + format + test) using deno task prep
  • Open Pull Request

License

Published under MIT License