diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5f19d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +package-lock.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..74487fb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Alain Perkaz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index ce1570b..05e02c3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,25 @@ -# m2-exhibition-api -m2 public api +# Express OpenAPI + +Code for the article: + +This codebase shows how to build an [OpenAPI](https://www.openapis.org/)-backed [express](https://expressjs.com/) application. + +## How to run + +```bash +# Install dependencies +npm i + +# Run app +npm run start +``` + +## Dynamic API documentation + +Thanks to its OpenAPI compliance, the app auto-generates the documenation of the API on the fly. + +Available in , while the app is running. + +![doc-image](./doc-image.png) + +## License diff --git a/api/api-doc.js b/api/api-doc.js new file mode 100644 index 0000000..51c8a28 --- /dev/null +++ b/api/api-doc.js @@ -0,0 +1,25 @@ +const apiDoc = { + swagger: "2.0", + basePath: "/", + info: { + title: "Todo app API.", + version: "1.0.0", + }, + definitions: { + Todo: { + type: "object", + properties: { + id: { + type: "number", + }, + message: { + type: "string", + }, + }, + required: ["id", "message"], + }, + }, + paths: {}, +}; + +module.exports = apiDoc; diff --git a/api/paths/todos/index.js b/api/paths/todos/index.js new file mode 100644 index 0000000..1c3ff17 --- /dev/null +++ b/api/paths/todos/index.js @@ -0,0 +1,112 @@ +module.exports = function () { + let operations = { + GET, + POST, + PUT, + DELETE, + }; + + function GET(req, res, next) { + res.status(200).json([ + { id: 0, message: "First todo" }, + { id: 1, message: "Second todo" }, + ]); + } + + function POST(req, res, next) { + console.log(`About to create todo: ${JSON.stringify(req.body)}`); + res.status(201).send(); + } + + function PUT(req, res, next) { + console.log(`About to update todo id: ${req.query.id}`); + res.status(200).send(); + } + + function DELETE(req, res, next) { + console.log(`About to delete todo id: ${req.query.id}`); + res.status(200).send(); + } + + GET.apiDoc = { + summary: "Fetch todos.", + operationId: "getTodos", + responses: { + 200: { + description: "List of todos.", + schema: { + type: "array", + items: { + $ref: "#/definitions/Todo", + }, + }, + }, + }, + }; + + POST.apiDoc = { + summary: "Create todo.", + operationId: "createTodo", + consumes: ["application/json"], + parameters: [ + { + in: "body", + name: "todo", + schema: { + $ref: "#/definitions/Todo", + }, + }, + ], + responses: { + 201: { + description: "Created", + }, + }, + }; + + PUT.apiDoc = { + summary: "Update todo.", + operationId: "updateTodo", + parameters: [ + { + in: "query", + name: "id", + required: true, + type: "string", + }, + { + in: "body", + name: "todo", + schema: { + $ref: "#/definitions/Todo", + }, + }, + ], + responses: { + 200: { + description: "Updated ok", + }, + }, + }; + + DELETE.apiDoc = { + summary: "Delete todo.", + operationId: "deleteTodo", + consumes: ["application/json"], + parameters: [ + { + in: "query", + name: "id", + required: true, + type: "string", + }, + ], + responses: { + 200: { + description: "Delete", + }, + }, + }; + + return operations; +}; diff --git a/app.js b/app.js new file mode 100644 index 0000000..25b1c10 --- /dev/null +++ b/app.js @@ -0,0 +1,39 @@ +var express = require("express"); +var path = require("path"); +var cookieParser = require("cookie-parser"); +var logger = require("morgan"); +var { initialize } = require("express-openapi"); +var swaggerUi = require("swagger-ui-express"); + +var app = express(); + +app.listen(3030); +app.use(logger("dev")); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); +app.use(cookieParser()); + +// OpenAPI routes +initialize({ + app, + apiDoc: require("./api/api-doc"), + paths: "./api/paths", +}); + +// OpenAPI UI +app.use( + "/api-documentation", + swaggerUi.serve, + swaggerUi.setup(null, { + swaggerOptions: { + url: "http://localhost:3030/api-docs", + }, + }) +); + +console.log("App running on port http://localhost:3030"); +console.log( + "OpenAPI documentation available in http://localhost:3030/api-documentation" +); + +module.exports = app; diff --git a/bin/www b/bin/www new file mode 100755 index 0000000..6f2541d --- /dev/null +++ b/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('express-open-api:server'); +var http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/doc-image.png b/doc-image.png new file mode 100644 index 0000000..0584682 Binary files /dev/null and b/doc-image.png differ diff --git a/express-open-api b/express-open-api new file mode 160000 index 0000000..20aecd9 --- /dev/null +++ b/express-open-api @@ -0,0 +1 @@ +Subproject commit 20aecd990858daa04e85aaa8ed772f6ae537ea53 diff --git a/package.json b/package.json new file mode 100644 index 0000000..7766a80 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "express-open-api", + "version": "0.0.0", + "engines": { + "node": "12.19.0", + "npm": "6.14.8" + }, + "private": true, + "scripts": { + "start": "node app.js" + }, + "dependencies": { + "cookie-parser": "~1.4.4", + "debug": "~2.6.9", + "express": "~4.16.1", + "express-openapi": "^7.5.0", + "morgan": "~1.9.1", + "swagger-ui-express": "^4.1.6" + } +}