Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/madeindjs/workflow.ts
TypeScript + Sequelize ORM + Express.js
https://github.com/madeindjs/workflow.ts
expressjs mermaid sequelize typescript
Last synced: 3 months ago
JSON representation
TypeScript + Sequelize ORM + Express.js
- Host: GitHub
- URL: https://github.com/madeindjs/workflow.ts
- Owner: madeindjs
- Created: 2019-06-13T15:23:41.000Z (about 5 years ago)
- Default Branch: master
- Last Pushed: 2022-12-10T19:01:00.000Z (over 1 year ago)
- Last Synced: 2024-01-17T07:33:13.415Z (5 months ago)
- Topics: expressjs, mermaid, sequelize, typescript
- Language: TypeScript
- Homepage: https://rousseau-alexandre.fr/en/programming/2019/06/19/express-typescript.html
- Size: 378 KB
- Stars: 20
- Watchers: 2
- Forks: 8
- Open Issues: 9
-
Metadata Files:
- Readme: README.md
Lists
- awesome-github-star - workflow.ts
README
# graph_api.ts
The purpose of this article is to discover an implementation of an[API][api][RESTfull][rest] using[TypeScript][typescript].
> TypeScript is a free and open source programming language developed by Microsoft to improve and secure the production of JavaScript code. (...) . The TypeScript code is transcompiled into JavaScript (...) TypeScript allows optional static typing of variables and functions (...) while maintaining the non-binding approach of JavaScript. [Wikipedia - TypeScript](https://fr.wikipedia.org/wiki/TypeScript)
We will therefore set up a very basic _graph_api_ system. We will create two models:
- a **node** (node) which represents a simple step. It just contains a `name' and an`id'.
- a **link** (link) that connects only two nodes with attributes `from_id' and`to_id'.It's as simple as that.
To build the API, I will use[Express.js], a minimalist framework that allows us to make APIs in JavaScript. At the end of the article, the API will be able to generate a definition of a [Mermaid][mermaid] graph which allows to convert the graph_api into a beautiful graph like the one below:
![Mermaid example](http://rich-iannone.github.io/DiagrammeR/img/mermaid_1.png)
_Let's go_!
> NOTE: I'm going to go a little fast because it's a bit of a reminder for myself. All the code is available on the [_repository_ Github `graph_api.ts`][github_repo]
> TL;DR: Express great freedom allows us to decide for ourselves the architecture of our application and TypeScript gives us the possibility to create real _design paterns_.
## Setup project
Let's create a brand new project using [NPM](https://www.npmjs.com/) and [Git versionning](https://git-scm.com/).
```bash
$ mkdir graph_api.ts
$ cd graph_api.ts/
$ npm init
$ git init
```Then we start to install somes dependencies to build the HTTP server:
```bash
$ npm install --save express body-parser
$ npm install --save-dev typescript ts-node @types/express @types/node
```As we use [TypeScript][typescript] we need to create a `tsconfig.json` to indicate how transcript TypeScript files from `lib` to `dist` folder. Also we transpile to [ES6][es6] version:
```json
// tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"pretty": true,
"sourceMap": true,
"target": "es6",
"outDir": "./dist",
"baseUrl": "./lib"
},
"include": ["lib/**/*.ts"],
"exclude": ["node_modules"]
}
```Now we we'll create the the `lib/app.ts`. This will be in charge to configure, load routes and start Express server:
```typescript
// lib/app.ts
import * as express from "express";
import * as bodyParser from "body-parser";
import { Routes } from "./config/routes";class App {
public app: express.Application;
public routePrv: Routes = new Routes();constructor() {
this.app = express();
this.config();
this.routePrv.routes(this.app);
}private config(): void {
this.app.use(bodyParser.json());
this.app.use(bodyParser.urlencoded({ extended: false }));
}
}export default new App().app;
```As you can see, we load the routes to define the controllers and the routes to comply with the MVC model. Here is the first `NodesController` which will handle actions related to _nodes_ with an action `index:`
```typescript
// lib/controllers/nodes.controller.ts
import { Request, Response } from "express";export class NodesController {
public index(req: Request, res: Response) {
res.json({
message: "Hello boi"
});
}
}
```We will now separate the routes into a separate file:
```typescript
// lib/config/routes.ts
import { Request, Response } from "express";
import { NodesController } from "../controllers/nodes.controller";export class Routes {
public nodesController: NodesController = new NodesController();public routes(app): void {
app.route("/").get(this.nodesController.index);app.route("/nodes").get(this.nodesController.index);
}
}
```And a `lib/server.ts` file to start `App` object:
```ts
// lib/server.ts
import app from "./app";
const PORT = process.env.PORT || 3000;app.listen(PORT, () => console.log(`Example app listening on port ${PORT}!`));
```And that's it. You can start server using `npm run dev` and try API using cURL:
```bash
$ curl http://localhost:3000/nodes
{"message":"Hello boi"}
```## Setup sequelize
Sequelize is an [ORM (Object Relational Mapping)][orm] which is in charge to translate TypeScript object in SQL queries to save models. The [sequelize documentation about TypeScrypt](http://docs.sequelizejs.com/manual/typescript) is really complete but don't panics I'll show you how to implement with Express.
We start to add librairies:
```bash
$ npm install --save sequelize sqlite
$ npm install --save-dev @types/bluebird @types/validator @types/sequelize
```> You may notice I choose SQLite database because of simplicity but you can use MySQL or Postgres
Then we will create a _lib/config/database.ts_ file to setup Sequelize database system. For simplicity, I create a Sqlite database in memory:
```ts
// lib/config/database.ts
import { Sequelize } from "sequelize";export const database = new Sequelize({
database: "some_db",
dialect: "sqlite",
storage: ":memory:"
});
```Then we'll be able to create a **model**. We'll begin with **Node** model who extends Sequelize `Model` class:
```ts
// lib/models/node.model.ts
import { Sequelize, Model, DataTypes, BuildOptions } from "sequelize";
import { database } from "../config/database";export class Node extends Model {
public id!: number;
public name!: string;
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
}
// ...
```You can notice that I added two fields `createdAt` and `updatedAt` that[Sequelize][sequelize] will fill automatically.
Then, we configure the SQL schema of the table and call `Node.sync`. This will create a table in the Sqlite database.
```ts
// lib/models/node.model.ts
// ...
Node.init(
{
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true
},
name: {
type: new DataTypes.STRING(128),
allowNull: false
}
},
{
tableName: "nodes",
sequelize: database // this bit is important
}
);Node.sync({ force: true }).then(() => console.log("Node table created"));
```That's it!
## Setup Node controller
Now we setup database, let's create simple CRUD methods in controller. This means:
- `index` to show list of nodes
- `show` to show a node
- `create` to add a new node
- `update` to edit a node
- `delete` to remove a node### Index
```ts
// lib/controllers/nodes.controller.ts
import { Request, Response } from "express";
import { Node } from "../models/node.model";export class NodesController {
public index(req: Request, res: Response) {
Node.findAll({})
.then((nodes: Array) => res.json(nodes))
.catch((err: Error) => res.status(500).json(err));
}
}
```And setup route:
```ts
// lib/config/routes.ts
import { Request, Response } from "express";
import { NodesController } from "../controllers/nodes.controller";export class Routes {
public nodesController: NodesController = new NodesController();public routes(app): void {
// ...
app.route("/nodes").get(this.nodesController.index);
}
}
```You can try the route using cURL:
```bash
$ curl http://localhost:3000/nodes/
[]
```It's seem to work but we have not data in SQlite database yet. Let's continue to add them.
### Create
We'll first define an **interface** which define properties we should receive from POST query. We only want to receive `name` property as `String`. We'll use this interface to cast `req.body` object properties. This will prevent user to inject a parameters who we not want to save in database. This is a good practice.
```ts
// lib/models/node.model.ts
// ...
export interface NodeInterface {
name: string;
}
```Now back in controller. We simply call `Node.create` and pass params from `req.body`. Then we'll use **Promise** to handle some errors:
```ts
// lib/controllers/nodes.controller.ts
// ...
import { Node, NodeInterface } from "../models/node.model";export class NodesController {
// ...
public create(req: Request, res: Response) {
const params: NodeInterface = req.body;Node.create(params)
.then((node: Node) => res.status(201).json(node))
.catch((err: Error) => res.status(500).json(err));
}
}
```And setup route:
```ts
// lib/config/routes.ts
// ...
export class Routes {
// ...
public routes(app): void {
// ...
app
.route("/nodes")
.get(this.nodesController.index)
.post(this.nodesController.create);
}
}
```You can try the route using cURL:
```bash
$ curl -X POST --data "name=first" http://localhost:3000/nodes/
{"id":2,"name":"first","updatedAt":"2019-06-14T11:12:17.606Z","createdAt":"2019-06-14T11:12:17.606Z"}
```It's seem work. Let's try with a bad request:
```bash
$ curl -X POST http://localhost:3000/nodes/
{"name":"SequelizeValidationError","errors":[{"message":"Node.name cannot be null","type":"notNull Violation",...]}
```Perfect. We can now continue.
### Show
The show method have a little difference because we need an `id` as GET parameter. This means we should have an URL like this: `/nodes/1`. It's simple to make this when you build the route. There the implementation.
```ts
// lib/config/routes.ts
// ...
export class Routes {
// ...
public routes(app): void {
// ...
app.route("/nodes/:id").get(this.nodesController.show);
}
}
```Now we can get the `id` parameter using `req.params.id`. THen we simply use `Node.findByPk` method and handle when this Promise get a `null` value which mean the node was not found. In this case, we return a 404 response:
```ts
// lib/controllers/nodes.controller.ts
// ...
export class NodesController {
// ...
public show(req: Request, res: Response) {
const nodeId: number = req.params.id;Node.findByPk(nodeId)
.then((node: Node | null) => {
if (node) {
res.json(node);
} else {
res.status(404).json({ errors: ["Node not found"] });
}
})
.catch((err: Error) => res.status(500).json(err));
}
}
```Now it should be alright. Let's try it:
```bash
$ curl -X POST http://localhost:3000/nodes/1
{"id":1,"name":"first","createdAt":"2019-06-14T11:32:47.731Z","updatedAt":"2019-06-14T11:32:47.731Z"}
$ curl -X POST http://localhost:3000/nodes/99
{"errors":["Node not found"]}
```### Update
Update method seams like the previous one and need an `id` parameter. Let's build route:
```ts
// lib/config/routes.ts
// ...
export class Routes {
// ...
public routes(app): void {
// ...
app
.route("/nodes/:id")
.get(this.nodesController.show)
.put(this.nodesController.update);
}
}
```Now we'll use the `Node.update` method which take two parameters:
- an `NodeInterface` interface which contains properties to update
- an `UpdateOptions` interface which contains the SQL `WHERE` constraintThen `Node.update` return a `Promise` like many Sequelize methods.
```ts
// lib/controllers/nodes.controller.ts
import { UpdateOptions } from "sequelize";
// ...
export class NodesController {
// ...
public update(req: Request, res: Response) {
const nodeId: number = req.params.id;
const params: NodeInterface = req.body;const update: UpdateOptions = {
where: { id: nodeId },
limit: 1
};Node.update(params, update)
.then(() => res.status(202).json({ data: "success" }))
.catch((err: Error) => res.status(500).json(err));
}
}
```Get it? Let's try it:
```bash
$ curl -X PUT --data "name=updated" http://localhost:3000/nodes/1
{"data":"success"}}
```Beautiful. Let's continue.
### Destroy
Destroy method seams like the previous one and need an `id` parameter. Let's build route:
```ts
// lib/config/routes.ts
// ...
export class Routes {
// ...
public routes(app): void {
// ...
app
.route("/nodes/:id")
.get(this.nodesController.show)
.put(this.nodesController.update)
.delete(this.nodesController.delete);
}
}
```For `destroy` method we call `Node.destroy` we take a `DestroyOptions` interface like `Node.update`.
```ts
// lib/controllers/nodes.controller.ts
// ...
import { UpdateOptions, DestroyOptions } from "sequelize";export class NodesController {
// ...
public delete(req: Request, res: Response) {
const nodeId: number = req.params.id;
const options: DestroyOptions = {
where: { id: nodeId },
limit: 1
};Node.destroy(options)
.then(() => res.status(204).json({ data: "success" }))
.catch((err: Error) => res.status(500).json(err));
}
}
```Let's try it:
```bash
$ curl -X DELETE http://localhost:3000/nodes/1
```Perfect!
## Create the Link relationship
Now we want to create the second model: the link. It has two attributes:
- `from_id` which will be a link on the previous node
- `to_id` which will be a link on the next node### Setup CRUD
I will be quick on basic CRUD link implementation because this is the same than nodes CRUD:
The model:
```ts
// lib/models/node.model.ts
import { Model, DataTypes } from "sequelize";
import { database } from "../config/database";
import { Node } from "./node.model";export class Link extends Model {
public id!: number;
public fromId!: number;
public toId!: number;
// timestamps!
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
}export interface LinkInterface {
name: string;
fromId: number;
toId: number;
}Link.init(
{
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true
},
fromId: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false
},
toId: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false
}
},
{
tableName: "links",
sequelize: database
}
);Link.sync({ force: true }).then(() => console.log("Link table created"));
```Controller:
```ts
// lib/controllers/links.controller.ts
import { Request, Response } from "express";
import { Link, LinkInterface } from "../models/link.model";
import { UpdateOptions, DestroyOptions } from "sequelize";export class LinksController {
public index(_req: Request, res: Response) {
Link.findAll({})
.then((links: Array) => res.json(links))
.catch((err: Error) => res.status(500).json(err));
}public create(req: Request, res: Response) {
const params: LinkInterface = req.body;Link.create(params)
.then((link: Link) => res.status(201).json(link))
.catch((err: Error) => res.status(500).json(err));
}public show(req: Request, res: Response) {
const linkId: number = req.params.id;Link.findByPk(linkId)
.then((link: Link | null) => {
if (link) {
res.json(link);
} else {
res.status(404).json({ errors: ["Link not found"] });
}
})
.catch((err: Error) => res.status(500).json(err));
}public update(req: Request, res: Response) {
const linkId: number = req.params.id;
const params: LinkInterface = req.body;const options: UpdateOptions = {
where: { id: linkId },
limit: 1
};Link.update(params, options)
.then(() => res.status(202).json({ data: "success" }))
.catch((err: Error) => res.status(500).json(err));
}public delete(req: Request, res: Response) {
const linkId: number = req.params.id;
const options: DestroyOptions = {
where: { id: linkId },
limit: 1
};Link.destroy(options)
.then(() => res.status(204).json({ data: "success" }))
.catch((err: Error) => res.status(500).json(err));
}
}
```And routes:
```ts
// lib/config/routes.ts
import { Request, Response } from "express";
import { NodesController } from "../controllers/nodes.controller";
import { LinksController } from "../controllers/links.controller";export class Routes {
public nodesController: NodesController = new NodesController();
public linksController: LinksController = new LinksController();public routes(app): void {
// ...
app
.route("/links")
.get(this.linksController.index)
.post(this.linksController.create);app
.route("/links/:id")
.get(this.linksController.show)
.put(this.linksController.update)
.delete(this.linksController.delete);
}
}
```Now everything should work like node endpoints:
```bash
$ curl -X POST --data "fromId=420" --data "toId=666" http://localhost:3000/links
$ curl http://localhost:3000/links
[{"id":1,"fromId":420,"toId":666,"createdAt":"2019-06-18T11:09:49.739Z","updatedAt":"2019-06-18T11:09:49.739Z"}]
```It's seem good but you see what going wrong? Actually we setup `toId` and `fromId` to nonexisting nodes. Let's correct that.
### Relationships
With sequelize we can easily build relationships between model using `belongTo` & `hasMany`. Let's do that:
```ts
// lib/models/node.model.ts
import { Link } from "./link.model";
// ...Node.hasMany(Link, {
sourceKey: "id",
foreignKey: "fromId",
as: "previousLinks"
});Node.hasMany(Link, {
sourceKey: "id",
foreignKey: "toId",
as: "nextLinks"
});Node.sync({ force: true }).then(() => console.log("Node table created"));
```Then restart the server using `npm run dev`. You may see the SQL query who create base:
```sql
Executing (default): CREATE TABLE IF NOT EXISTS `links` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `fromId` INTEGER NOT NULL REFERENCES `nodes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, `toId` INTEGER NOT NULL REFERENCES `nodes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL);
```You see the difference? Sequelize create _foreign keys_ between nodes and links. Now we can't create a link with broken relationship:
```bash
$ curl -X POST --data "fromId=420" --data "toId=666" http://localhost:3000/links
{"name":"SequelizeForeignKeyConstraintError"
```Let's retry to create link with valid nodes:
```bash
$ curl -X POST --data "name=first" http://localhost:3000/nodes
{"id":1,"name":"first","updatedAt":"2019-06-18T11:21:38.264Z","createdAt":"2019-06-18T11:21:38.264Z"}
$ curl -X POST --data "name=second" http://localhost:3000/nodes
{"id":2,"name":"second","updatedAt":"2019-06-18T11:21:47.327Z","createdAt":"2019-06-18T11:21:47.327Z"}
$ curl -X POST --data "fromId=1" --data "toId=2" http://localhost:3000/links
{"id":1,"fromId":"1","toId":"2","updatedAt":"2019-06-18T11:22:10.439Z","createdAt":"2019-06-18T11:22:10.439Z"}
```Perfect! Sequelize allow you to do many things so I suggest you to take a look at [their documentation](http://docs.sequelizejs.com/manual/associations.html).
## Draw the graph
Now we'll use our model to draw a graph. To do that, using [Mermaid.js][mermaid] who generate beautiful graph from plan text definitions. A valid definition look something like this:
```mermaid
graph TD;1[first node];
2[second node];
3[third node];1 --> 2;
2 --> 3
```It's really simple. Let's do create a new route linked to a new
```ts
// lib/config/routes.ts
// ...
import { GraphController } from "../controllers/graph.controller";export class Routes {
public nodesController: NodesController = new NodesController();
public linksController: LinksController = new LinksController();
public graphController: GraphController = new GraphController();public routes(app): void {
app.route("/").get(this.graphController.mermaid);
// ...
}
}
```Then use Sequelize to get all the nodes and links to draw the graph.
I move all the code into a **Promise** to improve readability and error handling. It's a simple implementation so you might want to improve it (and you'd be right) but it's enough in my case.
```ts
// lib/controllers/build.controller.ts
import { Request, Response } from "express";
import { Link } from "../models/link.model";
import { Node } from "../models/node.model";export class GraphController {
public mermaid(_req: Request, res: Response) {
// we'll englobe all logic into a big promise
const getMermaid = new Promise((resolve, reject) => {
let graphDefinition = "graph TD;\r\n";Node.findAll({})
// get all nodes and build mermaid definitions like this `1[The first node]`
.then((nodes: Array) => {
nodes.forEach((node: Node) => {
graphDefinition += `${node.id}[${node.name}];\r\n`;
});
})
// get all links
.then(() => Link.findAll())
// build all link in mermaid
.then((links: Array) => {
links.forEach((link: Link) => {
graphDefinition += `${link.fromId} --> ${link.toId};\r\n`;
});resolve(graphDefinition);
})
.catch((err: Error) => reject(err));
});// call promise and return plain text
getMermaid
.then((graph: string) => res.send(graph))
.catch((err: Error) => res.status(500).json(err));
}
}
``````bash
$ curl -X POST --data "name=first" http://localhost:3000/nodes &&
curl -X POST --data "name=second" http://localhost:3000/nodes &&
curl -X POST --data "name=third" http://localhost:3000/nodes &&
curl -X POST --data "fromId=1" --data "toId=2" http://localhost:3000/links &&
curl -X POST --data "fromId=2" --data "toId=3" http://localhost:3000/links
~~~And try the result output:
~~~bash
$ curl http://localhost:3000
graph TD;
1[first];
2[second];
3[third];
1 --> 2;
2 --> 3;
```Beautiful!
## Go further
We just build foundations of a graph_api system. We can easily con further. Here some ideas:
- build unit test
- add a names to links
- add a names to links
- add a `Graph` object who own some nodes
- add an authentification using JWT tokenAs you can see, ExpressJS is a toolbox that interfaces very well with TypeScript. Express' great freedom allows us to decide for ourselves the architecture of our application and TypeScript gives us the possibility to create real _design paterns_.
## Liens
- https://glebbahmutov.com/blog/how-to-correctly-unit-test-express-server/
- https://www.techighness.com/post/unit-testing-expressjs-controller-part-1/
- https://blog.logrocket.com/a-quick-and-complete-guide-to-mocha-testing-d0e0ea09f09d/[mermaid]: https://github.com/knsv/mermaid
[typescript]: https://www.typescriptlang.org/
[es6]: https://en.wikipedia.org/wiki/ECMAScript#6th_Edition_-_ECMAScript_2015
[orm]: https://en.wikipedia.org/wiki/Object-relational_mapping
[rest]: https://en.wikipedia.org/wiki/Representational_state_transfer
[api]: https://en.wikipedia.org/wiki/Application_programming_interface
[express]: https://expressjs.com/
[sequelize]: http://docs.sequelizejs.com
[mocha]: https://mochajs.org/
[github_repo]: https://github.com/madeindjs/graph_api.ts