blog
blog copied to clipboard
Documenting and building Node.js APIs on IBM i
In this blog, I will explain my stages for documenting and building APIs in Node.js.
There are three main stages:
- Documenting the API
- Creating the models / entities
- Developing the API
Documenting the API
Documenting the API is almost a requirement today when building APIs.
- It allows the business to decide what each API should do
- It defines how the APIs should be organized
- You're able to share the API specs so others can start to implement calling them before they are live
- It makes developing the API a ton easier because you know how the API should work
Documenting is quite a hard scenario. You should definitely use OpenAPI 3.0 (or previously known as Swagger). It allows you to define what a RESTful API should do and look like. The problem with OpenAPI 3.0 is that is can be quite daunting if you haven't seen it before:
paths:
/users:
get:
summary: Returns a list of users.
description: Optional extended description in CommonMark or HTML.
responses:
'200': # status code
description: A JSON array of user names
content:
application/json:
schema:
type: array
items:
type: string
The above REST API returns a simple JSON array of strings. It is important that before building your API, you define it with the OpenAPI spec. This means you can decide on functionality before implementing so you spend less time re-writing.
The other part is that OpenAPI can also define models, which you can think of re-usable structures in your APIs. For example, let's say we have a User model. The User model contains a name, email and phone number. We might have two APIs:
-
GET /users
- which returns an array of our User model -
GET /users/:id
- might return a single User model
paths:
/users:
get:
summary: List of Users
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
operationId: get-users
'/users/{id}':
parameters:
- schema:
type: string
name: id
in: path
required: true
get:
summary: Single User by ID
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/User'
operationId: get-users-id
components:
schemas:
User:
title: User
type: object
properties:
name:
type: string
email:
type: string
phone:
type: string
description: User structure
Notice in this API spec, our User model lives in the Components property. The problem I have is that I really don't enjoy manually writing YAML. There are lots of tools for building, maintaining and viewing OpenAPI specifications, but my favourite is Stoplight. It was shown to me by my friend Connor and it has changed my life. It makes building APIs fun.
Creating the Models / Entities / Using an ORM
Using an ORM, or at least encapsulating your database access and business logic makes writing web APIs a ton easier. It allows you to separate out the web API (Express) logic from any database or business logic. It means that you can also re-use your business logic and database access across multiple APIs instead of writing it over and over again.
There lots of different ORMs and ways to create Models for your data (TypeORM, mongoose, Sequelize). Since I am primarily working with Db2 for i, I have the struggle that Db2 isn't a well-supported database with those ORMs.
Previously I was manually creating classes for all my models and data access. I was getting frustrated with re-writing a lot of the stuff and wanted something to create everything for me (kind of like a traditional ORM would) - so I built db2Model.
db2Model will build classes based on a Db2 for i table for me. It will create
- each column as a property in the class (with the correct JavaScript types and column documentation)
- static methods to get a model (row) by primary or unique key(s).
- instance methods to update and delete the row based on the properties.
- instance methods to fetch other Models based on foreign keys in the column
Here's an example model for the DEPARTMENT
table in the Db2 for i Sample schema.
const db2 = require('../db2');
const GenericTable = require('./GenericTable');
const Employee = require('./Employee');
module.exports = class Department extends GenericTable {
static _table = 'DEPARTMENT';
static _schema = 'SAMPLE';
static _keys = ["DEPTNO"];
static _columns = ["DEPTNO","DEPTNAME","MGRNO","ADMRDEPT","LOCATION"];
constructor(row) {
super();
/** @type {String} DEPTNO */
this.deptno = row.DEPTNO.trim();
/** @type {String} DEPTNAME */
this.deptname = row.DEPTNAME;
/** @type {String} MGRNO */
this.mgrno = (row.MGRNO ? row.MGRNO.trim() : null);
/** @type {String} ADMRDEPT */
this.admrdept = row.ADMRDEPT.trim();
/** @type {String} LOCATION */
this.location = (row.LOCATION ? row.LOCATION.trim() : null);
}
/**
* Fetchs Employee by mgrno
* @returns {Employee} Returns new instance of Employee
*/
async getEmployee() {
return await Employee.Get(this.mgrno);
}
/**
* Fetchs Department by admrdept
* @returns {Department} Returns new instance of Department
*/
async getDepartment() {
return await Department.Get(this.admrdept);
}
Delete() {
//Code removed to preserve space
}
Update() {
//Code removed to preserve space
}
/**
* Fetchs Department by keys
* @param {String} deptno
* @returns {Department} Returns new instance of Department
*/
static async Get(deptno) {
const row = await this.Find({deptno}, 1);
return (row.length === 1 ? row[0] : null);
}
}
All that code is self-generated from db2Model. Notice, that it extends the GenericTable
class. That parent class has two extra methods:
-
Find
which allows us to query our table with a where clause and limit -
Join
which allows us to query and join to another table by it's Model too.
It means that in our express APIs we can just inherit and use our Model to do all the work. Of course, we can add more instance methods to our model to do more work too!
app.get('/departments', async (req, res) => {
const depts = await Department.Find();
res.json(depts); //Returns array of Department models
});
app.get('/departments/:id', async (req, res) => {
const deptno = req.params.id;
const dept = await Department.Get(deptnp);
res.json(dept); //Returns single Department model
});
app.post('/department/:id', async (req, res) => {
//Simple update API - should add more validation checks
const deptno = req.params.id;
const newLocation = req.body.location; //New location
try {
const department = Department.Get(deptno);
department.location = newLocation;
department.Update();
res.status(200).json({
message: "Update successful."
});
} catch (e) {
res.status(500).json({
message: "Update failed."
});
}
});
Developing the API
I think I've posted enough code examples. The important part about building the API is that your web APIs match your OpenAPI spec.
That's where https://www.npmjs.com/package/express-openapi-validator can help. You can build your OpenAPI specification with Stoplight, define the validator in your express application and link it to your OpenAPI specification which validates all input and output based on your definitions.
await new OpenApiValidator({
apiSpec: './test/resources/openapi.yaml',
validateRequests: true, // (default)
validateResponses: true, // false by default
}).install(app);
The page linked above has a great example of an OpenAPI spec and an express application.