my avatar
English | 日本語
All articles↩

Making Web APIs Type-safe with Aspida


Types matter. Even with the naive type system of C/C++, I used to feel somehow protected.

TypeScript enables for JavaScript libraries to provide their type definitions but Web APIs. Actually, majority of APIs lack precise documentation, let alone type definition.

Aspida fills this blind spot. Few HTTP client libraries on NPM provide this capability.

In this article, I demonstrate how we can invoke Discourse API as an example in type-safe manner.

Setting up Aspida

You can setup Aspida according the official README.

Since Aspida provides just an abstraction layer for type-safety, you need to chose one of HTTP client library as its back-end. Major libraries (axios, ky, fetch and node-fetch) seem to be supported.

Here, I pick axios.

Overview of Discourse API

You may know that Discourse is an open source discussion platform.

I try accessing an instance of Discourse via its Web API and show the names of visible Categories in the forum for demonstration.

Discourse API is a simple RESTful API, with nice and sufficient documentation. I'm not so fluent about the API, but it seems to cover almost all of its functionalities.

Since I'm the Server Admin of a Mastodon server, I chose Mastodon Meta Discussion Board for an example :)

Creating Type definition

First of all, we need the type definition.

You can assign types for the response and request parameters of your favorite APIs with Aspida, by putting type definition files in $REPO_ROOT/apis/ like:

export type CategoryList = {
    can_create_category: boolean,
    can_create_topic: boolean,
    draft: boolean,
    draft_key: string,
    draft_sequence: number,
    categories: Category[]

export type Category = {
    id: number,
    name: string,
    color: string,
    text_color: string
    slug: string,
    topic_count: number,
    post_count: number,
    position: number,
    description: string,
    description_text: string,
    topic_url: string,
    logo_url: string,
    background_url: string,
    read_restricted: boolean,
    permission: number,
    notification_level: string,
    can_edit: boolean,
    topic_template: string,
    has_children: boolean,
    topics_day: number,
    topics_week: number,
    topics_month: number,
    topics_year: number,
    topics_all_time: number,
    description_excerpt: string,

export type Methods = {
    get: {
        resBody: {
            category_list: CategoryList

This time I put this as categories.ts.

This is a hand-crafted type definition :tm: looking up the API documentation :muscle:

Building the Type definition

Once you have created the type definition, you need to build before using in application:

$ yarn run aspida --build

You may be happier by having this defined in package.json.

Invoking the API in Application

Now you can invoke the API in type-safe manner! You can write your application like below.

import dotenv from "dotenv"
import axios from "axios"
import aspida from "@aspida/axios"
import api from "../apis/$api"
import type { CategoryList } from "../apis/categories"


const axiosConfig = {
    timeout: 3000,
    baseURL: '',
    headers: {
        'Accept': 'application/json',
//        'Api-Username': process.env.DISCOURSE_API_USERNAME,
//        'Api-Key': process.env.DISCOURSE_API_KEY,

let client = api(aspida(axios, axiosConfig))

;(async () => {
        .then(response => {
            let category_list =
            category_list.categories.forEach(cat => console.log(
        .catch(error => console.log(error))

It seems you need to import types explicitly if you want enable code-completion.

Also, you can pass Axios options at instantiation of Aspida. I had confirmed it worked well with headers for authentication.

This results:

$ yarn run start
Server administration
Core development
App development
Meta feedback
Done in 10.56s.

Looks good :+1:

You can see complete code at:

You're now embraced in type system, so you won't break a thing like:

category_list.categories.forEach(cat => console.log( * 1))

because this will be detected at compile time:

src/discourse-list-category.ts:25:65 - error TS2362: The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.

25             category_list.categories.forEach(cat => console.log( * 1))


Aspida has another interesting functionality which automatically build type definitions from OpenAPI Specification.

Since Discourse API provides it, we'll try this in next article :)

This post is also available at:
@tnzk © 2010-2024