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.
+
+
+
+## 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"
+ }
+}