DEV Community

Cover image for ApiBlaze: Designing the Search API for Properties & Endpoints
Sebastian
Sebastian

Posted on

ApiBlaze: Designing the Search API for Properties & Endpoints

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.

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.

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.

function apiElementsDetailsSearchAction (object) {
  switch (object.type) {
    case 'object':
      return loadObject(object)
    case 'property': 
      return loadProperty(object)
    case 'endpoint':
      return loadEndpoint(object)
}
Enter fullscreen mode Exit fullscreen mode

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:

{
  "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"
  ]
}
Enter fullscreen mode Exit fullscreen mode

The frontend consumes this structure and creates the following representation:

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:

{
  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
}
Enter fullscreen mode Exit fullscreen mode

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:

"/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"
        }
      },
      // ...
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

"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"
  },
}
Enter fullscreen mode Exit fullscreen mode

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.


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") +
}
Enter fullscreen mode Exit fullscreen mode

Here is an example:

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.

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

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

"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."
    }
  },
  //...
}
Enter fullscreen mode Exit fullscreen mode

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

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.

Top comments (0)