ApiBlaze: Designing the Search API for Properties & Endpoints
Tags: javascript, openapi
Published: 2021-03-08 06:50:29

_ApiBlaze is a tool to explore API specifications: Search for a keyword, filter for objects, properties, or endpoints, and immediately see descriptions and code examples. ApiBlaze helps you to answer a specific question about an API lightning fast. **You can try it here: [apiblaze.admantium.com](https://apiblaze.admantium.com).**_

When searching for API elements, you will find objects, properties and endpoints. When selecting them, they will be displayed differently: objects show their data model, properties a list of ranked objects in which they appear, and endpoints show request parameters as well as complex response objects.

In this article, we will cover the modifications required for displaying properties and endpoints, and finish the ApiBlaze development journey.

_This article originally appeared at [my blog](https://blog.admantium.com/blog/api10_search_api_properties_and_endpoints/)_.

## Handling Requests for Loading Details

When the user clicks on a search result, the frontend requests to load the details from the backend. The backend distinguishes which type of object is requested, and calls a specific handler method.

```js
function apiElementsDetailsSearchAction (object) {
  switch (object.type) {
    case 'object':
      return loadObject(object)
    case 'property': 
      return loadProperty(object)
    case 'endpoint':
      return loadEndpoint(object)
}
```

Let' continue to see how properties are loaded.

## Search & Render Properties

For a property, we need its name, description, type, and a list of all objects that use this property. This information is gathered with the following steps:

- Load the containing object of the property
- Load the definition of the property inside the containing object
- Search in all objects if they contain the property, and save all these object names

The resulting data structure is this:

```json
{
  "name": "imagePullSecrets",
  "containingObject": "io.k8s.api.core.v1.ServiceAccount",
  "type": "Property",
  "attrType": "array",
  "description": "ImagePullSecrets is a list of references to secrets in the same namespace to use for pulling any images in pods that reference this ServiceAccount. ImagePullSecrets are distinct from Secrets because Secrets can be mounted in the pod, but ImagePullSecrets are only accessed by the kubelet. More info: https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod",
  "containingObjectList": [
    "io.k8s.api.core.v1.ServiceAccount",
    "io.k8s.api.core.v1.PodSpec"
  ]
}
```

The frontend consumes this structure and creates the following representation:

![](https://blog.admantium.com/images/blog/api10_example_properties.png)

## Search & Render Endpoints

Endpoints are a unique entity that requires special treatment. When an API specification is loaded, the endpoints will be indexed and separated along their HTTP methods. For example, when both GET and POST methods are accepted, two entries will be created. Here is an example:

```json
{
  name: "POST /api/v1/namespaces/{namespace}/pods",
  containingObject: "/api/v1/namespaces/{namespace}/pods",
  type: "Endpoint",
  description: "create a Pod",
  score: 3
},
{
  name: "GET /api/v1/namespaces/{namespace}/pods",
  containingObject: "/api/v1/namespaces/{namespace}/pods",
  type: "Endpoint",
  description: "list or watch objects of kind Pod",
  score: 3
}
```

Both search items reference the same endpoint specification. From this specification, we need to extract the relevant information.

The steps are quite complex, so let's start from the beginning: The original OpenAPI spec. The `post` endpoint looks like this:

```json
"/api/v1/namespaces/{namespace}/pods": {
  "post": {
    "consumes": [
      "*/*"
    ],
    "description": "create a Pod",
    "operationId": "createCoreV1NamespacedPod",
    "parameters": [
      {
        "in": "body",
        "name": "body",
        "required": true,
        "schema": { 
          // ... 
        }
      },
      {
        "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed",
        "in": "query",
        "name": "dryRun",
        "type": "string",
        "uniqueItems": true
      },
      // ....
    ],
    "responses": {
      "200": {
        "description": "OK",
        "schema": {
          "$ref": "#/definitions/io.k8s.api.core.v1.Pod"
        }
      },
      // ...
    },
  }
}
```

Endpoints have two interesting pieces of information: `parameters` and `responses`.

### Processing Endpoint Parameters

Parameters can be passed as query parameters, added to the URLs. Or they can be passed inside the request body as the JSON payload. While query parameters are simple key-value pairs, body parameters are the nested, complex objects that were covered in the last article.

Parameters are processed with these steps:

- Filter all parameters which have the property `in === 'query'`
  - For these items, only store the attributes `description` and `type`
- Filter all parameters if there is a single item with the property `in === 'body'`
  - For this item, process its nested `schema` attribute

Applying these transformations to the above mentioned `post` endpoint leads the following data structure:

```js
"queryParameters": [
  "dryRun": {
    "_description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed",
    "_type": "string",
  },
  "fieldManager": {
    "_description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.",
    "_type": "string",
  }
  ]
},
"bodyParameters": {
  "apiVersion": {
    "_type": "string",
    "_description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources"
  },
  "kind": {
    "_type": "string",
    "_description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds"
  },
}
```

Body Parameters and query parameters will be rendered in their own code boxes. As before, the JSON structure will be converted to a string and several HTML transformations will be applied.

```js

renderEndpoint() {
  const { bodyParameters, queryParameters } = this.getState().searchApiElementDetails
  
  document.querySelector(this.querySelector).innerHTML = 
    this.style(this.format(bodyParameters), "Body Parameters") +
    this.style(this.format(queryParameters), "Query Parameters") +
}
```

Here is an example:

![](https://blog.admantium.com/images/blog/api10_example_query_and_body_parameters.png)


### Processing Endpoint Responses

In the original OpenAPI spec, responses are mapping of HTTP status codes to objects with a `description` and a `schema`. Here is an example for the status code `200`.

```json
"/api/v1/namespaces/{namespace}/pods": {
  "post": {
    // ...
    "responses": {
      "200": {
        "description": "OK",
        "schema": {
          "$ref": "#/definitions/io.k8s.api.core.v1.Pod"
        }
      },
      // ...
    },
  }
}
```

The optional element `schema` points to a nested object which will be processed. The resulting data structure is this:

```json
"responses": {
  "200": {
    "_description": "OK",
    "properties": {
      "_type": "object",
      "_description": "Pod is a collection of containers that can run on a host. This resource is created by clients and scheduled onto hosts.",
      "apiVersion": {
        "_type": "string",
        "_description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources"
      },
      "kind": {
        "_type": "string",
        "_description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds"
      }
      //...
    }
  },
  "201": {
    "_description": "Created",
    "properties": {
      "_type": "object",
      "_description": "Pod is a collection of containers that can run on a host. This resource is created by clients and scheduled onto hosts."
    }
  },
  //...
}
```

When rendered, each status code is a separate section with the nested object shown in a code box.

![](https://blog.admantium.com/images/blog/api10_example_responses.png)

## ApiBlaze Project Requirements are Finished

With these changes completed, we have fulfilled all ApiBlaze requirements:

- Searching for APIS
  - ✅ SEA01 - Search for APIs by Keyword
  - ✅ SEA02 - Show search results in a popup
  - ✅ SEA03 - Select a search results with arrow keys, enter and mouse click
- Searching API Elements
  - ✅ SEL01 - Distinguish objects, properties and endpoints
  - ✅ SEL02 - Search for API Elements by keywords
  - ✅ SEL03 - Show search results in a popup
  - ✅ SEL04 - Select a search results with arrow keys, enter and mouse click
- Display API Elements
  - ✅ DIS01 - Show an objects description
  - ✅ DIS02 - When an object is selected: Show its entire data model
  - ✅ DIS03 - When a property is selected: Show in which objects it is used
  - ✅ DIS04 - When an endpoint is selected: Show its request and response object
- Framework
  - ✅ FRAME01 - Controller & Routing
  - ✅ FRAME02 – Stateful Pages & Components
  - ✅ FRAME03 - Actions
  - ✅ FRAME04 – Optimized Bundling
- Technologies
  - ✅ TECH01 - Use PlainJS & Custom Framework
  - ✅ TECH02 - Use SAAS for CSS
  - ✅ TECH03 - Use WebSockets to Connect Frontend and Backend

## Conclusion

The ApiBlaze development journey was long and knowledge intensive. Initially prototyped in the middle of 2020, I restarted the project after a long break. The requirements evolved: In addition to core functions, I also wanted to use WebSockets and use a custom framework to deepen my knowledge. Not entirely surprising, developing a framework became journey of its own, and a very rewarding one to deepen JavaScript knowledge. When I read about the other JavaScript frameworks, and see how they are working, I can better relate to the features they have and how they help to design applications. Finally, I'm happy to have completed this journey, and hopefully you will use ApiBlaze to quickly search in API specifications.