Building an API with TypeScript, Express.js and Sequelize

  • express
  • sequelize
  • typescript

Publié le 2019-06-19

Intéressé pour créer un API avec Typescript / Node.js? Jette un coup d'œil à mon livre: REST-API.ts. Tu peux télécharger une version gratuite au format PDF sur Github. Si tu aimes mon travail tu peux acheter un version payante sur Leanpub.

The purpose of this article is to discover an implementation of anAPIRESTfull usingTypeScript.

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 transpiled into JavaScript (...) TypeScript allows optional static typing of variables and functions (...) while maintaining the non-binding approach of JavaScript. Wikipedia - 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 anid`.
  • a link (link) that connects only two nodes with attributes from_id' andto_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 graph which allows converting the graph_api into a beautiful graph like the one below:

Mermaid example

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

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 and Git versionning.

mkdir graph_api.ts
cd graph_api.ts/
npm init
git init

Then we start to install some dependencies to build the HTTP server:

npm install --save express body-parser
npm install --save-dev typescript ts-node @types/express @types/node

As we use TypeScript we need to create a tsconfig.json to indicate how transcript TypeScript files from lib to dist folder. Also we transpile to ES6 version:

// 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:

// 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:

// 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:

// 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:

// 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 the server using npm run dev and try API using cURL:

curl http://localhost:3000/nodes
{"message":"Hello boi"}

Setup sequelize

Sequelize is an ORM (Object Relational Mapping) which is in charge to translate TypeScript objects in SQL queries to save models. The sequelize documentation about TypeScrypt is really complete but don't panics I'll show you how to implement with Express.

We start to add librairies:

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:

// 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:

// 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 thatSequelize 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.

// 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 set up the database, let's create simple CRUD methods in the controller. This means:

  • index to show a 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

// 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<Node>({})
      .then((nodes: Array<Node>) => res.json(nodes))
      .catch((err: Error) => res.status(500).json(err));
  }
}

And setup route:

// 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:

curl http://localhost:3000/nodes/
[]

It's seems 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 defines 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 parameters who we do not want to save in the database. This is a good practice.

// lib/models/node.model.ts
// ...
export interface NodeInterface {
  name: string;
}

Now back in the controller. We simply call Node.create and pass params from req.body. Then we'll use Promise to handle some errors:

// 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<Node>(params)
      .then((node: Node) => res.status(201).json(node))
      .catch((err: Error) => res.status(500).json(err));
  }
}

And setup route:

// 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:

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:

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 has a little different 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.

// 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 gets a null value which means the node was not found. In this case, we return a 404 response:

// lib/controllers/nodes.controller.ts
// ...
export class NodesController {
  // ...
  public show(req: Request, res: Response) {
    const nodeId: number = req.params.id;

    Node.findByPk<Node>(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:

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

The update method seems like the previous one and needs an id parameter. Let's build a route:

// 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 takes two parameters:

  • a NodeInterface interface which contains properties to update
  • an UpdateOptions interface which contains the SQL WHERE constraint

Then Node.update returns a Promise like many Sequelize methods.

// 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:

curl -X PUT --data "name=updated" http://localhost:3000/nodes/1
{"data":"success"}}

Beautiful. Let's continue.

Destroy

Destroy method seems like the previous one and need an id parameter. Let's build a route:

// 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.

// 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:

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 as nodes CRUD:

The model:

// 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:

// 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<Link>({})
      .then((links: Array<Link>) => res.json(links))
      .catch((err: Error) => res.status(500).json(err));
  }

  public create(req: Request, res: Response) {
    const params: LinkInterface = req.body;

    Link.create<Link>(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<Link>(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:

// 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:

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 seems good but you see what goes 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:

// 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:

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:

curl -X POST --data "fromId=420" --data "toId=666"  http://localhost:3000/links
{"name":"SequelizeForeignKeyConstraintError"

Let's retry to create link with valid nodes:

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 allows you to do many things so I suggest you to take a look at their documentation.

Draw the graph

Now we'll use our model to draw a graph. To do that, use Mermaid.js who generates a beautiful graph from plan text definitions. A valid definition looks something like this:

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

// 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.

// 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<string>((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<Node>) => {
          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<Link>) => {
          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));
  }
}
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 names to links
  • add names to links
  • add a Graph object who own some nodes
  • add an authentification using JWT token

As 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.

Links

Articles liés