node.js-graphic--releases-image

API services are a common component of Node.js applications. This installment of the ongoing Node.js reference architecture series focuses on the development of REST APIs using an API-first approach.

Read the series so far:

The IBM and Red Hat team has provided a comprehensive reference for developers looking to build Node.js applications. The Markdown source file of the reference specification is also available on GitHub under the Apache 2.0 open source license.

In the typical process of building API services, developers start by implementing their endpoints and exposing them using a REST API. After the API is deployed, the team often faces challenges documenting and sharing information about that API. This article lays out the most common approaches to designing and implementing REST APIs, culminating in what we call the API-first approach.

Methods for building a REST API

Teams building and documenting a REST API usually employ one of the following methods:

  1. Create the API implementation first, then manually add documentation as a document, web page, or OpenAPI spec. Changes and updates call for special attention to keep endpoints and documentation in sync, because documentation cannot be automatically validated. The quality of the documentation depends on the attention to detail given by the developers who maintain it.
  2. Annotate code in order to generate an OpenAPI spec for the consumers served by the REST endpoints. This approach is often called "code-first" because the OpenAPI file is created by processing information in the code.
  3. Generate an OpenAPI file during development and publish it to the source repository. The specification file is accessible without running the service. However, clients and integrators cannot modify the specification in order to propose new changes.
  4. Create an OpenAPI file as input to the API design process. The file can be used as a scaffold for different clients, SDKs, command-line interfaces (CLIs), and even backends. This approach is called "API-first" or "schema-first" and is being widely adopted by teams building large sets of APIs for a wide range of clients.

Each of these methods represents a different maturity level in regard to maintenance, discoverability, and automation. Table 1 lists the key traits of each method.

Table 1: There are four levels in the API maturity model.
1. Manual documentation 2. OpenAPI from code 3. OpenAPI in code 4. OpenAPI first
  • Prone to errors, missing arguments etc.
  • Hard to update and maintain
  • Weak API contract due to manual API design
  • Limited paths to automation
  • Limited ways to report issues, propose new changes, or improve API
  • Clients need to be written by hand
  • OpenAPI spec is as good as code annotations provided by developers
  • Generated OpenAPI file missing details and it is often suboptimal for client generation
  • Lack of visibility for various stakeholders before API is implemented and deployed
  • Simple automation of API client generation
  • Simple implementation of API validation using tools like Spectral
  • No ability to propose changes and edit generated file
  • Requires CI/CD workflow to keep file up to date
  • Ability to negotiate API at the design level
  • Possibility to use API design tools like Apicurio Studio, Swagger Editor, etc.
  • Workflow focused on code generation and scaffolding, minimizing errors and ensuring a strong API
  • Editable OpenAPI file in the repository

What is an API contract and why is it important?

An API contract is a qualitative measure of how well an API's documentation reflects the real state of the API. The contract is strong if the documentation is up-to-date and accurate, and weak when the documentation diverges significantly from the real service.

A strong API contract meets the following criteria:

  • Adopters have a full list of operations, inputs, and outputs documented by an OpenAPI file or other standards.
  • Developers can track and detect breaking changes.
  • The API is properly versioned using semantic versioning, such as X.Y.Z to represent major version (X), minor version (Y), and release (Z).
  • The API describes and defines all error states. An error state is an inconsistent or invalid situation, and should not be confused with an HTTP status code.
  • The API contains examples that are up to date and reflect all possible states of the API.

Each level of the API maturity model in Table 1 provides a stronger API contract than the previous ones.

API-first approach

In the API-first approach, the highest level of the API maturity model, designing the API is the first priority and is done before writing any code. The approach involves thorough thinking and planning through collaboration with different teams on both the client and backend sides.

A good design process results in high-level documentation describing the intent of the API. One important impact is that API consumers can build clients without waiting for the server to be finished.

This API contract acts as a central draft keeping all your team members aligned on your API's objectives and on how to expose your API's resources. The finalization of the contract allows the team to build the interface of the application.

After the interface is published, cross-functional teams rely on it to build the rest of the application, independent of each other. In practice, teams can generate backend stubs and fully functional client libraries to avoid deviating from the specification in the OpenAPI file.

Code-first versus API-first

The code-first approach provides libraries that understand the server-side backend structure and generates respective OpenAPI files. Full control over API lies within the server-side team. Therefore, the generated OpenAPI file is read-only and cannot be effectively used by client developers to negotiate API features with the server-side team.

Although the code-first approach is often seen as good practice and is adopted by many teams, it doesn't provide a very strong API contract. Code doesn't contain enough metadata and information to generate compatible documentation. Concepts in the code, such as inheritance, are often not well reflected in OpenAPI.

The API-first approach uses OpenAPI file as the source of truth. Both client-side and server-side developers write code based on the OpenAPI file. API-first gives the unique ability for clients and backends to start development at the same time at the cost of initial API design and validation phases.

After many experiments and implementations using both methods, the Node.js reference architecture team recommends the API-first approach. The reference document mentioned at the beginning of this article includes a recommendation for following an API-first approach when building APIs.

The API-first development process

The API-first approach might lead to modifications in your team's development workflow. Figure 1 shows a typical workflow for developing a REST API.

Diagram illustrating that a development workflow covers design, validate, generate, implement, and release phases.
Figure 1: A development workflow covers design, validate, generate, implement, and release phases.

Developers can design and propose new versions of the API as GitHub pull requests (PRs) or GitLab merge requests, which can be peer-reviewed. Proposed changes can also be validated by PR builds, GitHub actions, etc. Some teams use external systems such as the Apicurio Registry to hold proposed versions of the schemas and to facilitate early adoption across teams.

Once an API is approved, it can be used to generate early artifacts that enable both client and backend developers to start implementing features backed by the new API. Every correction of the API specification restarts the process and automatically notifies every developer about the API changes. Once all development is finished, code changes along with the API definitions are merged to the main branch in order to be released to the API consumers.

Challenges with the API-first approach

Adhering to an API-first workflow requires a certain discipline from teams, calling on them to think about API consumers and business requirements when building the API specification. This high-level thinking helps keep the API free from implementation details, but introduces additional overhead on the implementation of the backend. An API-first approach also requires a wider group of API stakeholders to understand OpenAPI specifications.

Writing OpenAPI using JSON and YAML is often error-prone. Teams can avoid errors by using web editors or VScode plugins to help designers validate API during the design process.

Introducing API-first into an organization often requires changes to its development model. API-first delays feature development until the design is finished and the API is reviewed and validated.

The API-first process requires engagement from different teams and tools. The method is being adopted by teams who have a wide range of experience with building CI/CD workflows that can automate API validation. API-first works well for teams that use code-sharing repositories such as GitHub or GitLab for the code and OpenAPI review process.

Summary

We hope we've gotten you excited about using an API-first approach on your next Node.js project and given you the tools to get started. For specific recommendations, check out the REST API Development section in the Node.js reference architecture.

We plan to cover new topics regularly as part of the Node.js reference architecture series. Until the next installment, we invite you to visit the Node.js reference architecture repository on GitHub, where you will see the work we have done and look forward to future topics. To learn more about what Red Hat is up to on the Node.js front, check out our Node.js page.

Last updated: August 14, 2023