Building a Node API with API-first design

What is API-first design and how do you approach it as a Node developer?

2 years ago   •   6 min read

By Anthony Gore
Table of contents

Now that APIs are a key aspect of the software industry, the concept of API-first design is gaining traction.

Under this paradigm, an API definition is created and collaborated on before any code is written.

This enables stakeholders to ensure the API meets their objectives as well as
streamline the development phase once the API design is finalized.

API-first design requires new tools, like the OpenAPI 3 standard, as well as new ways of working and new best practices.

In this article, we’ll look at API-first design from a developer’s perspective. We’ll get a better understanding of API-first design principles and see what tools support API-first design in the Node.js ecosystem.

Why API-first design?

It used to be that an API was added to a project as an afterthought. In the era of the API economy, however, interconnected APIs are the backbone of many applications.

It’s therefore important for organizations to ensure there is sufficient planning and
collaboration with stakeholders on the design of an API.

Under the API-first design paradigm, stakeholders from across an organization will collaborate on a central API definition before the development phase begins.
Some of the advantages of this approach include:

  • Improved collaboration.

APIs now need to meet the demands of complex data and integrations, while also being efficient across devices, platforms, and operating systems. API-first design allows multiple stakeholders to provide feedback on the API to ensure it fulfills these key purposes.

  • Parallel development.

Once the development phase begins, development teams can work in parallel with the confidence that they’re working on a settled API contract.

For example, one team could work on implementing the API in code while another can be working on creating mocks and tests, while yet another can work on client applications.

  • Automated code generation.

By using a spec like OpenAPI 3, developers can utilize powerful scaffolding tools that allow the creation of code, tests, and documentation through automated generator software.

OpenAPI 3

To allow collaboration on an API under the API-first paradigm, stakeholders will work together on a document that defines the API (often called a “contract”).

The most popular spec for doing this is OpenAPI 3. This is an open-source format
designed to describe RESTful APIs.

It’s easy to write and easy for stakeholders to provide feedback on. Here’s a trivial example of an OpenAPI 3 definition written with YAML (though JSON
can also be used).

openapi: 3.0.0
info:
	version: 1.0.0
	title: Petstore
	description: A sample API to illustrate OpenAPI concepts
paths:
	/pets:
	  get:
		description: Returns all pets
		responses:
		  '200':
			description: ok

// petstore.yml

💡
Note: While it’s possible to write definitions by hand, it’s easier to
work in visual environments like OpenAPI-GUI.

An important thing to notice from this example is that the definition includes meta-information about the API (like the name and description) as well as the technical specification of API paths and their responses.

Path example

Let’s now take a look at how we’d add a more complex path GET /pets/{id} to an
OpenAPI 3 definition.

Some of the noteworthy properties include:

  • operationId is an optional unique string used to identify an operation. As you’ll see below, code generators use this value to name corresponding methods.

  • parameters defines input parameters. In this case, a dynamic path parameter.

  • responses defines the API response. In this case, we’ve shown the 200 responseand the returned properties. In a real example, we’d likely include error responses as well.

paths:
	/pets/{id}:
	  get:
		description: Get a specific pet
		operationId: getPetById
		parameters:
		  - name: id
			in: path
			required: true
			schema:
			type: integer
		responses:
		  '200':
			description: ok
			content:
			  application/json:
			    schema:
				  type: object
				  properties:
				    name:
					  type: string
					id:
					  type: integer
					type:
					  type: string

Development phase

Once the stakeholders have finalized the API design, developers will begin
implementing it in code. This not only includes the API application, but also tests, mocks, and documentation.

Of course, an OpenAPI 3 definition can be implemented in any web-facing programming language like Python, Ruby, or C#. In this article, though, we’ll be providing examples using Node.js and Express.

Let’s begin by defining a basic Express server module to which we’ll add our OpenAPI routes to shortly.

const express = require('express')
const app = express()
app.use(express.json())
// API goes here
module.exports = app

app.js

OpenAPI Backend

One of the less-obvious advantages of API-first design is that it provides opportunities for automated code scaffolding as the definition can serve as a blueprint for API code and tests.

OpenAPI Backend is one such tool that allows for scaffolding routes in an OpenAPIbased app.

Let’s use it in our demo project. Firstly, we load the Open API 3 definition (assuming it’s a YAML file called petstore.yml), and pass it to the OpenAPI Backend constructor.

Next, we register a controller module that will handle the scaffolded routes. We’ll look at this more in the next section.

We then initialize the backend and load the API as Express middleware.

const OpenAPIBackend = require('openapi-backend').default
// 1. Create API with definition file
const api = new OpenAPIBackend({ definition: './petstore.yml' })
// 2. Register controllers
api.register(require('./controllers.js'))
// 3. Initialize backend
api.init()
// 4. Load as express middleware
app.use((req, res) => api.handleRequest(req, req, res))

Here’s the full Express app. Note that this app will now have routes and validated
requests corresponding to the OpenAPI definition saving developers from manually coding these.

const express = require('express')
const OpenAPIBackend = require('openapi-backend').default
const app = express()
app.use(express.json())
const api = new OpenAPIBackend({ definition: './petstore.yml' })
api.register(require('./controllers.js'))
api.init()
app.use((req, res) => api.handleRequest(req, req, res))
module.exports = app

app.js

Controllers

How do we hook into our OpenAPI routes with controller methods?

You may recall that the GET /pets/{id} path we defined in the OpenAPI definition had an operationId property of getPetById.

Code generators like OpenAPI Backend uses this ID as a key for assigning a handler
method for the routes it defines. More specifically, defining a method getPetById will provide us with a callback for the GET /pets/{id} path.

As with any Express controller, you would typically use this callback to unpack the context,req, and res objects and pass off to business logic and the data layer.

💡
You can read more on this in the article How to structure an Express.js REST API with best practices.

Unlike regular Express callbacks, however, OpenAPI Backend handlers are passed a special Context object as the first argument, which contains the parsed request, the matched API operation, and input validation results.

module.exports = {
  getPets: (context, req, res) => {
	console.log(context.operation)
    /*
	 {
	  method: get,
	  path: '/pets',
	  operationId: 'getPets'
	  summary: 'Returns all pets'
	  ...
	  }
    */
    console.log(context.validation)
    /*
    {
	  valid: true,
	  errors: null
     }
     */
     
	// Business logic
	const pets = await DB.getPets()
...

	res.json(pets)
   },
   getPetById: (context, req, res) => {
...
   }
  }

// controllers.js

Testing

We can also use our OpenAPI definition to power API tests. The OpenAPIValidators library, for example, provides plugins like jest-openapi for the Jest JavaScript test runner.

Let’s see how we’d use jest-openapi to test the Express app we defined above. For this we’ll use Jest and Supertest to make test HTTP calls to our app.

jest-openapi example

In our test file, we first require Supertest and the Express app module. We can then import jest-openapi and our OpenAPI definition.

Let’s now write a test case for the GET /pets/{id} endpoint where we’ll verify that the response satisfies the OpenAPI spec.

First, we get an HTTP response by using the get method of the Supertest request API.

Next, we make an assertion expect(res).toSatisfyApiSpec(). The toSatisfyApiSpec assertion is provided by the jest-openapi plugin and ensures an HTTP response satisfies the provided OpenAPI definition.

// 1. Require supertest and server app
const request = require('supertest')
const app = require('./app.js')

// 2. Import Jest OpenAPI plugin
import jestOpenAPI from 'jest-openapi'

// 3. Load OpenAPI definition
jestOpenAPI('./petstore.yml');

// 4. Test
describe('GET /pets/{id}', () => {
  it('should satisfy OpenAPI spec', async () => {
  
    // 5. Get an HTTP response from supertest
    const res = await request(app).get('/pets/1')
   
   // 6. Assert that the HTTP response satisfies the OpenAPI spec
   expect(res).toSatisfyApiSpec();
  });
});

Now we can run this test from command line:

$ npx jest app.test.js

API monitoring with Treblle

If APIs are crucial to your organization, it’s important to implement an API observability tool into your workflow.

This will allow you to learn what’s happening from within your API so that root cause analysis can be performed quickly when problems occur.

Treblle is API observability software that can be installed in an Express app with just a few lines of code. It will monitor and trace any issues with your API so that they can be caught and fixed before affecting your uses.

In addition to API observation, Treblle offers your API a variety of other useful features including:

  • Auto-generated API documentation
  • API quality scores
  • 1 click testing
💡
Treblle is free for up 30,000 requests and can be installed in minutes, so give it a try!

Wrap up

It used to be that organizations added APIs to a app as an afterthought. Now that APIs are a key driver of applications, the concept of API-first design is growing in popularity.

Under this new paradigm, an API definition is created using specs like OpenAPI 3.
Stakeholders will then collaborate on the API before any code is written to ensure it meets organizational objectives.

The API-first design paradigm can result in a variety of benefits including improved collaboration between teams, parallel development, and automated code generation.

Spread the word

Keep reading