In this exericse, we will explore the starter hello-world Node.js application built with express and oas-tools and perform following tasks:
- Include OpenAPI spec for the to-do list API.
- Implement endpoints for the API based on the specificaiton.
- Implement unit tests for the exposed endpoints.
Git clone the api-first-workshop-nodejs project to your local machine and install the dependencies:
git clone https://github.com/rhappdev/api-first-workshop-nodejs
cd api-first-workshop-nodejs
npm install
npm run dev
The app already contains the sample code for Hello World API based on OpenAPI specification located at ./src/definition/swagger.yaml. You can execute a curl command curl http://localhost:8001/api/hello to verify that the server is working. You should receive an error response:
[{"message":"Missing parameter greeting in query. "}]
- Download the specification for ToDo List API from Apicurio that we genereated in Exercise 1.
- Replace the contents of the file located at
./src/definition/swagger.yamlwith the ToDo List API specification.
Note: In case if you have not been able to finish exercise-1, please download the complete ToDo List API specification from here
npm run dev
Review the ToDo List API's resources through Swagger UI located at http://localhost:8001/docs.
oas-tools looks for a special annotation in the .yaml file to map the endpoint to its Express router and controller when executing a request.
In swagger.yaml, define the controller for the operation getItems by adding a property x-swagger-router-controller: itemRoute just before operationId: getItems.
Likewise, please add the same property for remaining operations: createItem, getItem, updateItem and deleteItem.
In swagger.yaml, add x-name: item property for createItem and updateItem operation just after the requestBody property, one level down (the indentation shold be the same as the property description inside requestBody).
OpenAPI Specification version 3 defines request's body in a different way. It is not a parameter as it was in Swagger version 2. Request bodies are defined in tne section
requestBodywhich doesn't have property calledname. It needs this property to work with the code that is genereated usingopenapi-generator-clitool.
npm run test
Review the tests written in /test/application.spec.ts.
-
Create a directory
src/controllersand a filegetItemsHander.tsinside, and update it with following code:import { Request, Response } from "express"; import * as P from "bluebird"; import { TDebug } from "../log"; import { ItemDAO } from "../dao/item.dao"; const debug = new TDebug("nodejs:src:controllers:getItemsHandler"); const itemDao = ItemDAO.getInstance(); export async function getItemsHandler(req: Request, res: Response): P<any> { debug.log("getItemsHandler start"); const items = itemDao.getItems(); res.send(items); debug.log("getItemsHandler end"); }
Note: Data models and data access objects (DAO) are already defined as part of the base template.
- Implement the rest of the controllers for the operations/endpoints defined in the ToDo List OpenAPI spec. For reference, the solution has been provided in the branch
step2of the api-first-workshop-nodejs repository.
-
Create a directory
src/routesand a new fileitemRoute.tsinside it with the following content:-
Include reference for
expressrouter andasyncHandler.import { Router } from "express"; import { asyncHandler } from "../lib/asyncHandler";
-
Map the controllers with the corresponding route. Add the following lines for the
getItemsoperation:import { getItemsHandler } from "../controllers/getItemsHandler"; export const getItems = Router().use("/", asyncHandler(getItemsHandler, "getItems"));
-
Add the corresponding lines for all other operations according to the ToDo API specification. Refer to the implemented
itemRoute.tsfile in thestep2branch of the api-first-workshop-nodejs repository. -
In the file
src/middlewares/swagger.tsin theinitSwaggerMiddlwarechange the optionrouter: falsetorouter: true. This enables the router middleware.
-
Note: All the parameters and body objects are accessible through
req.swagger.params.xxx.valueattribute.
Start the app:
npm run dev
Go to http://localhost:8001/docs and make some API calls, you should receive 200 OK status code.
-
Create a directory
test/routesand a fileitem.route.spec.tsinside it. -
Update the file
item.route.spec.tswith the following test case:import chaiHttp = require("chai-http"); import app from "../../src/application"; import { ItemDAO } from "../../src/dao/item.dao"; import * as chai from "chai"; const expect = chai.expect; chai.use(chaiHttp); describe("ItemRoute - Test ItemRoute endpoints", function () { it("getItems - Should retrieve and empty Array", (done: () => void): void => { chai.request(app) .get("/api/v1/items") .set("content-type", "application/json") .send({}) .end((err: Error, res: any): void => { expect(res.statusCode).to.be.equal(200); expect(res.body.length).to.be.equal(0); done(); }); }); it("getItems - Should retrieve and Item Array with one Item", (done: () => void): void => { const itemDAO = ItemDAO.getInstance(); itemDAO.addItem({"description": "Description", name: "Name", id: "id"}); chai.request(app) .get("/api/v1/items") .set("content-type", "application/json") .send({}) .end((err: Error, res: any): void => { expect(res.statusCode).to.be.equal(200); expect(res.body.length).to.be.equal(1); expect(res.body[0].name).to.be.equal("Name"); expect(res.body[0].description).to.be.equal("Description"); itemDAO.deleteItem(res.body[0].id); done(); }); }); });
npm run test
-
Create a directory
test/daoand a fileitem.dao.spec.tsinside it. -
Update the file
item.dao.spec.tswith following test case:import { ItemDAO } from "../../src/dao/item.dao"; import * as chai from "chai"; import { Item } from "../../src/models/item"; const expect = chai.expect; describe("ItemDAO - Test ItemDAO Methods", function () { const itemDAO = ItemDAO.getInstance(); it("getItems - Should retrieve an empty array", (done: () => void): void => { expect(itemDAO.getItems().length).to.be.equal(0); done(); }); it("addItem - Should add an item", (done: () => void): void => { itemDAO.addItem({"description": "Description", name: "Name", id: "id"}); expect(itemDAO.getItems().length).to.be.equal(1); expect(itemDAO.getItems()[0].description).to.be.equal("Description"); expect(itemDAO.getItems()[0].name).to.be.equal("Name"); done(); }); it("getItems - Should retrieve an item", (done: () => void): void => { expect(itemDAO.getItems().length).to.be.equal(1); done(); }); it("getItem - Should retrieve an item", (done: () => void): void => { const item = itemDAO.getItems()[0]; expect(itemDAO.getItem(item.id)).not.to.be.equal(undefined); done(); }); it("getItem - Should not retrieve an item", (done: () => void): void => { expect(itemDAO.getItem("xxxx-ssssss-sssss")).to.be.equal(undefined); done(); }); it("updateItem - Should update an item", (done: () => void): void => { const item = itemDAO.getItems()[0]; item.description = "Description 2"; item.name = "Name 2"; itemDAO.updateItem(item); expect(itemDAO.getItems()[0].name).to.be.equal("Name 2"); expect(itemDAO.getItems()[0].description).to.be.equal("Description 2"); done(); }); it("deleteItem - Should delete an item", (done: () => void): void => { const item = itemDAO.getItems()[0]; itemDAO.deleteItem(item.id); expect(itemDAO.getItems().length).to.be.equal(0); done(); }); });
npm run test
-
Start the API back end:
npm run dev -
In the local copy of the
api-first-workshop-angularapplication, insrc/app/environments/environment.tschange theAPI_URLtohttp://localhost:8001/api/v1. -
Start the Angular app
ionic serve
and go to http://localhost:8100
Verify that the application works as expected.