├── doc-image.png
├── package.json
├── api
├── api-doc.js
└── paths
│ └── todos
│ └── index.js
├── README.md
├── app.js
├── LICENSE
├── .gitignore
└── bin
└── www
/doc-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aperkaz/express-open-api/HEAD/doc-image.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "express-open-api",
3 | "version": "0.0.0",
4 | "engines": {
5 | "node": "12.19.0",
6 | "npm": "6.14.8"
7 | },
8 | "private": true,
9 | "scripts": {
10 | "start": "node app.js"
11 | },
12 | "dependencies": {
13 | "cookie-parser": "~1.4.4",
14 | "debug": "~2.6.9",
15 | "express": "~4.16.1",
16 | "express-openapi": "^7.5.0",
17 | "morgan": "~1.9.1",
18 | "swagger-ui-express": "^4.1.6"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/api/api-doc.js:
--------------------------------------------------------------------------------
1 | const apiDoc = {
2 | swagger: "2.0",
3 | basePath: "/",
4 | info: {
5 | title: "Todo app API.",
6 | version: "1.0.0",
7 | },
8 | definitions: {
9 | Todo: {
10 | type: "object",
11 | properties: {
12 | id: {
13 | type: "number",
14 | },
15 | message: {
16 | type: "string",
17 | },
18 | },
19 | required: ["id", "message"],
20 | },
21 | },
22 | paths: {},
23 | };
24 |
25 | module.exports = apiDoc;
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Express OpenAPI
2 |
3 | Code for the article:
4 |
5 | This codebase shows how to build an [OpenAPI](https://www.openapis.org/)-backed [express](https://expressjs.com/) application.
6 |
7 | ## How to run
8 |
9 | ```bash
10 | # Install dependencies
11 | npm i
12 |
13 | # Run app
14 | npm run start
15 | ```
16 |
17 | ## Dynamic API documentation
18 |
19 | Thanks to its OpenAPI compliance, the app auto-generates the documenation of the API on the fly.
20 |
21 | Available in , while the app is running.
22 |
23 | 
24 |
25 | ## License
26 |
27 | MIT © [Alain Perkaz](https://aperkaz.github.io)
28 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | var express = require("express");
2 | var path = require("path");
3 | var cookieParser = require("cookie-parser");
4 | var logger = require("morgan");
5 | var { initialize } = require("express-openapi");
6 | var swaggerUi = require("swagger-ui-express");
7 |
8 | var app = express();
9 |
10 | app.listen(3030);
11 | app.use(logger("dev"));
12 | app.use(express.json());
13 | app.use(express.urlencoded({ extended: false }));
14 | app.use(cookieParser());
15 |
16 | // OpenAPI routes
17 | initialize({
18 | app,
19 | apiDoc: require("./api/api-doc"),
20 | paths: "./api/paths",
21 | });
22 |
23 | // OpenAPI UI
24 | app.use(
25 | "/api-documentation",
26 | swaggerUi.serve,
27 | swaggerUi.setup(null, {
28 | swaggerOptions: {
29 | url: "http://localhost:3030/api-docs",
30 | },
31 | })
32 | );
33 |
34 | console.log("App running on port http://localhost:3030");
35 | console.log(
36 | "OpenAPI documentation available in http://localhost:3030/api-documentation"
37 | );
38 |
39 | module.exports = app;
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Alain Perkaz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
--------------------------------------------------------------------------------
/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | var app = require('../app');
8 | var debug = require('debug')('express-open-api:server');
9 | var http = require('http');
10 |
11 | /**
12 | * Get port from environment and store in Express.
13 | */
14 |
15 | var port = normalizePort(process.env.PORT || '3000');
16 | app.set('port', port);
17 |
18 | /**
19 | * Create HTTP server.
20 | */
21 |
22 | var server = http.createServer(app);
23 |
24 | /**
25 | * Listen on provided port, on all network interfaces.
26 | */
27 |
28 | server.listen(port);
29 | server.on('error', onError);
30 | server.on('listening', onListening);
31 |
32 | /**
33 | * Normalize a port into a number, string, or false.
34 | */
35 |
36 | function normalizePort(val) {
37 | var port = parseInt(val, 10);
38 |
39 | if (isNaN(port)) {
40 | // named pipe
41 | return val;
42 | }
43 |
44 | if (port >= 0) {
45 | // port number
46 | return port;
47 | }
48 |
49 | return false;
50 | }
51 |
52 | /**
53 | * Event listener for HTTP server "error" event.
54 | */
55 |
56 | function onError(error) {
57 | if (error.syscall !== 'listen') {
58 | throw error;
59 | }
60 |
61 | var bind = typeof port === 'string'
62 | ? 'Pipe ' + port
63 | : 'Port ' + port;
64 |
65 | // handle specific listen errors with friendly messages
66 | switch (error.code) {
67 | case 'EACCES':
68 | console.error(bind + ' requires elevated privileges');
69 | process.exit(1);
70 | break;
71 | case 'EADDRINUSE':
72 | console.error(bind + ' is already in use');
73 | process.exit(1);
74 | break;
75 | default:
76 | throw error;
77 | }
78 | }
79 |
80 | /**
81 | * Event listener for HTTP server "listening" event.
82 | */
83 |
84 | function onListening() {
85 | var addr = server.address();
86 | var bind = typeof addr === 'string'
87 | ? 'pipe ' + addr
88 | : 'port ' + addr.port;
89 | debug('Listening on ' + bind);
90 | }
91 |
--------------------------------------------------------------------------------
/api/paths/todos/index.js:
--------------------------------------------------------------------------------
1 | module.exports = function () {
2 | let operations = {
3 | GET,
4 | POST,
5 | PUT,
6 | DELETE,
7 | };
8 |
9 | function GET(req, res, next) {
10 | res.status(200).json([
11 | { id: 0, message: "First todo" },
12 | { id: 1, message: "Second todo" },
13 | ]);
14 | }
15 |
16 | function POST(req, res, next) {
17 | console.log(`About to create todo: ${JSON.stringify(req.body)}`);
18 | res.status(201).send();
19 | }
20 |
21 | function PUT(req, res, next) {
22 | console.log(`About to update todo id: ${req.query.id}`);
23 | res.status(200).send();
24 | }
25 |
26 | function DELETE(req, res, next) {
27 | console.log(`About to delete todo id: ${req.query.id}`);
28 | res.status(200).send();
29 | }
30 |
31 | GET.apiDoc = {
32 | summary: "Fetch todos.",
33 | operationId: "getTodos",
34 | responses: {
35 | 200: {
36 | description: "List of todos.",
37 | schema: {
38 | type: "array",
39 | items: {
40 | $ref: "#/definitions/Todo",
41 | },
42 | },
43 | },
44 | },
45 | };
46 |
47 | POST.apiDoc = {
48 | summary: "Create todo.",
49 | operationId: "createTodo",
50 | consumes: ["application/json"],
51 | parameters: [
52 | {
53 | in: "body",
54 | name: "todo",
55 | schema: {
56 | $ref: "#/definitions/Todo",
57 | },
58 | },
59 | ],
60 | responses: {
61 | 201: {
62 | description: "Created",
63 | },
64 | },
65 | };
66 |
67 | PUT.apiDoc = {
68 | summary: "Update todo.",
69 | operationId: "updateTodo",
70 | parameters: [
71 | {
72 | in: "query",
73 | name: "id",
74 | required: true,
75 | type: "string",
76 | },
77 | {
78 | in: "body",
79 | name: "todo",
80 | schema: {
81 | $ref: "#/definitions/Todo",
82 | },
83 | },
84 | ],
85 | responses: {
86 | 200: {
87 | description: "Updated ok",
88 | },
89 | },
90 | };
91 |
92 | DELETE.apiDoc = {
93 | summary: "Delete todo.",
94 | operationId: "deleteTodo",
95 | consumes: ["application/json"],
96 | parameters: [
97 | {
98 | in: "query",
99 | name: "id",
100 | required: true,
101 | type: "string",
102 | },
103 | ],
104 | responses: {
105 | 200: {
106 | description: "Delete",
107 | },
108 | },
109 | };
110 |
111 | return operations;
112 | };
113 |
--------------------------------------------------------------------------------