How to REST API: a tale of Node.js, Express and Typescript

Simone Staffa
Mar 22 · 13 min read
Image for post
Image for post

What is a REST API?

REST API is extensively considered as the standard protocol for the web APIs.

REpresentational State Transfer (REST) is a set of principles that define how Web standards are supposed to be used, which often differs quite a bit from what many people actually do.

The key goals of REST include:

  • Scalability of component interactions
  • Generality of interfaces
  • Independent deployment of components
  • Intermediary components to reduce latency, enforce security and encapsulate legacy systems.

In REST interactions are client-server and stateless, this means that history is not accumulated with time, giving an advantage in term of scalability. Components expose a uniform interface: GET, POST, PUT, DELETE (actually the most popular HTTP verbs).

In REST is crucial the concept of resources and their identification. Each resource must have an ID (usually a URI) and it can be any object the API can provide information about. On Facebook API, for example, a resource can be a user, a photo, a post.

A RESTful web application exposes information about itself in the form of information about its resources. Such an application also enables the client to take actions on those resources, such as create new resources (i.e. create a new post) or change existing resources (i.e. edit a post).

For example, when a developer calls Facebook API to fetch a specific post (the resource), the API will return the details of that post, including its content, the image or video (if any), the author, the reactions and more.

The representation of the state of the resource can be expressed in two main different formats: JSON and XML. In this article, I will use the JSON which is the common one.

Image for post
Image for post

The process of interaction will be the following:

  1. The client will call the APIs endpoints exposed by the server, specifying the resource in which it is interested in (the URL), and the method it wants the server to perform (GET, POST, PUT, DELETE)
  2. The server responds with, returning to the client the result of the operation is requested.

This was only a brief introduction for what you should know to understand the code and the process that I’m gonna describe in this article.

Hands-on Code

As you are reading this tutorial, I’m assuming that you are familiar with HTTP protocol, Node.js, NPM and Typescript. You are not required to know them, as I will guide you during each step, but if you have already used them in the past, for sure this tutorial will be easier to follow.

Let’s say that we want to create an API to support a fake web application based on Rotten Tomatoes. We will call it Rotten Potatoes! :)

First of all, we have to create a new folder. I’ll call it “rotten-potatoes-api”. From the terminal, getting into our new folder, assuming that we have npm installed locally into our path, we type:

mkdir rotten-potatoes-api 
cd rotten-potatoes-api
npm init -y

Typing that command will create a new package.json into our folder, which is a file used to store project details and external modules (that will be contained inside the famous “node_modules” folder).

Then we need to install and initialize a bunch of modules to support Typescript.

npm install -g typescript 
npm install --save-dev tslint tsc ts-node typescript
tsc --init
tslint --init

As a result, now you should have two new files into the folder: tslint.json and tsconfig.json. The former is used to specify the syntax checks that you want the compiler to perform on TypeScript code, to improve readability, maintainability, and functionality errors. The latter is used to specify the compiler options (as you may know, in the end, we will compile the Typescript code into Javascript).

For simplicity just override them with these two snippets:

## ~/rotten-potatoes-api/tsconfig.json
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",
"noImplicitAny": true,
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"baseUrl": ".",
"paths": {
"*": [
"node_modules/*"
]
}
},
"include": [
"src/**/*"
]
}
## ~/rotten-potatoes-api/tslint.json{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"rules": {
"trailing-comma": [ false ],
"no-console": false,
"variable-name": false,
"no-empty-interface": false
},
"rulesDirectory": []
}

Now we create a folder called src, where we will store all our Typescript classes source code that will need to be compiled. Following, inside that folder we create a structure of subfolders, ending to have a something like this:

Image for post
Image for post
Project folder

We will start by writing our app.ts file inside the “src” folder. This file will be the point in which our express routing functionality is initialized.

Before to start, let’s install express and body-parser, and also their typescript version:

npm install --save express body-parser
npm install --save-dev @types/express @types/body-parser

Note that with body-parser, we’re letting the express router parse the HTTP body into JSON, as this will be the format that we will use.

Then inside our app.ts, we will import the express module, and we will initialize it. Following the express documentation, we then declare our first endpoint

## ~/rotten-potatoes-api/src/app.tsimport express from 'express';
import * as bodyParser from 'body-parser';
const app = express();
app.use(bodyParser.json({
limit: '50mb',
verify(req: any, res, buf, encoding) {
req.rawBody = buf;
}
}));
app.get('/', (req, res) => res.send('Hello World!'));
export {app};

Here is the first version of the app.ts file, which is very basic and contains only a simple endpoint, which, when called, responds with a “Hello World” message. In particular, the “app.get” function, indicates to the express router to handle a GET method over the “/” path, and we will see in a moment how to test it.

Then we go on with index.ts, where we will expose the express process on our local machine:

## ~/rotten-potatoes-api/src/index.tsimport {app} from './app'
import {AddressInfo} from 'net'
const server = app.listen(5000, '0.0.0.0', () => {
const {port, address} = server.address() as AddressInfo;
console.log('Server listening on:','http://' + address + ':'+port);
});

Before trying our first run, be sure to fix your package.json in this way (for the sake of simplicity I’ll specify only the fields that you need to be sure to have modified:

## ~/rotten-potatoes-api/package.json{
"...",
"main": "dist/index.js",
"scripts": {
"prebuild": "rm -rf dist/* && tslint -c tslint.json -p tsconfig.json --fix",
"build": "tsc && cp -rf package.json dist/package.json",
"prestart": "npm run build",
"start": "ts-node ."
},
"...",
}

Great! We can now start our process by running from the terminal, inside our root path:

npm run start

Running that command will trigger the execution in order of the scripts:

  • prebuild
  • build
  • prestart
  • start

which will build our project inside the “dist” folder, and run it only if everything was ok.

You should now see in your terminal the message:

“Server listening on: http://0.0.0.0:5000

We can check that everything is working by using a simple HTTP client like Postman or by the command line using curl to send a GET to http://localhost:5000.

curl http://localhost:5000/

As output, you should receive the “Hello world!” message that we’ve set before.

Since in REST, as we previously said, interactions are stateless (the history of resource’s state is not maintained), we need a database to store the resources with which we are interacting with.

In this article, I will use SQLite because in my opinion is the simplest one to use, even if it is not suggested for production environments. By the way, if you are familiar with other databases’ technologies (e.g., MySQL, MongoDB, Postgres and others), you can use your preferred one, as the module that we’re gonna include into the project (typeorm) supports most of them.

We also need to install sqlit3 and then initialize our database:

npm install --save-dev typeorm
npm install --save sqlite3
sqlite3 database.db

Then, inside the sqlite3 console, just type

sqlite> .databases

As a result, if the database was correctly created you should see something like this:

sqlite> .databases
main: ~/<your_path_here>/rotten-potatoes-api-basic/database.db

After exiting from the sqlite3 console, you should have into the root folder, a file named database.db. That of course, represent the file in which our data will be saved.

Now we can create our database component inside the “src/db” folder, following the typeorm guides.

## ~/rotten-potatoes-api/src/db/db.tsimport {createConnection} from "typeorm";export let connect = async () => {
const connection = await createConnection({
"type": "sqlite",
"database": "database.db",
"name": "default",
"synchronize": false,
"logging": true,
"migrationsTableName": "custom_migration_table",
"entities": [
__dirname + '/models/*.js'
],
"migrations": [
__dirname + '/migrations/*.js'
],
"cli": {
entitiesDir: __dirname + "/models/",
migrationsDir: __dirname + '/migrations/'
}
});
};

Now we need to initialize the database connection in the “app.ts”.

## ~/rotten-potatoes-api/src/app.tsimport * as bodyParser from 'body-parser';
import {connect} from "./db/db";
connect();const app = express();...export {app};

Right after that, we create two additional folders inside the db one: “models” and “migrations”.

The “models” folder will contain the classes that will map the database table into our codebase. For example, in our case, as we will see later on, we will have a model that maps the movies table (the one storing data about movies) into a specific class called Movie in our project.

The “migrations” folder will contain our database migrations to have our models in sync with the database. For those of you who don’t know what migration is, I’ll mention here the definition given in the typeorm documentation:

“A migration is a set of instructions that define precise steps to develop a new version of the database”

Why we need them? Whenever we decide to update our models, for example adding the column “rating” to our Movie class, using migrations allows us to:

  1. Avoid writing plain SQLite queries (or whatever language you are using)
  2. Store a history of the changes that we applied into our database (and also their order) so that it can be easy to synchronize a newly created database with the schema we’ve created so far.

For simplicity, in this tutorial, we will only deal with the Movie table. As said, we want to use migrations to track the database changes. Database migrations need as well a configuration to connect to our database. We then need to create a file called ormconfig.json into the root folder:

## ~/rotten-potatoes-api/ormconfig.json{
"type": "sqlite",
"database": "database.db",
"name": "default",
"synchronize": false,
"logging": true,
"migrationsTableName": "custom_migration_table",
"entities": [
"src/db/models/**/*.ts"
],
"migrations": [
"src/db/migrations/*.ts"
],
"cli": {
"entitiesDir": "src/db/models",
"migrationsDir": "src/db/migrations"
}
}

Then from the terminal, we can generate a migration for creating the Movie table.

typeorm migration:create --name CreateMovieTable

In case the typeorm command is not found. Try installing it locally with the command below, and then retry executing the “migration:create” command.

npm install -g typeorm

After the migration is successfully created, inside the migrations folder you should see a new file.

## /db/migrations/<timestamp>-CreateMovieTable.tsimport {MigrationInterface, QueryRunner} from "typeorm";export class CreateMovieTable1584641446366 implements MigrationInterface {    public async up(queryRunner: QueryRunner): Promise<any> {
}
public async down(queryRunner: QueryRunner): Promise<any> {
}
}

The method up and down respectively will be used for running the migration (up) or rolling it back (down). Using the typeorm module will be a lot easier creating the movie table in our database. We want our movie to have 4 fields: title, plot summary, duration (in minutes) and release date.

## /db/migrations/<timestamp>-CreateMovieTable.tsimport {MigrationInterface, QueryRunner, Table} from "typeorm";export class CreateMoviesTable1584289576471 implements MigrationInterface {    public async up(queryRunner: QueryRunner): Promise<any> {
return await queryRunner.createTable(new Table({
name: "movies",
columns: [
{
name: "id",
type: "integer",
isPrimary: true,
isGenerated: true,
generationStrategy: 'increment'
},
{
name: "title",
type: "varchar",
isNullable: false,
},
{
name: "plot_summary",
type: "text",
isNullable: false
},
{
name: "duration",
type: "integer",
isNullable: false
}
]
}), true);
}
public async down(queryRunner: QueryRunner): Promise<any> {
return await queryRunner.dropTable("movies");
}
}

As you can see, in the up method we are creating the table, while in the down method we are dropping it out. Another nice advantage of migrations is that if you made something wrong (e.g., wrong field type), you can just rollback it, fix the problem and run it again.

To run our migration we will add two additional script commands to our package.json:migration:run” andmigration:revert”. There is also a modification on the “build” script, that handles the database file copy inside the “dist” folder. This operation is needed because as mentioned before, the code will be executed from that folder.

## ~/rotten-potatoes-api/package.json{
"...",
"main": "dist/src/index.js",
"scripts": {
"prebuild": "rm -rf dist/* && tslint -c tslint.json -p tsconfig.json --fix",
"build": "tsc && cp -rf package.json dist/package.json && cp -rf database.db dist/database.db",
"prestart": "npm run build",
"start": "ts-node .",
"migration:run": "ts-node ./node_modules/typeorm/cli.js migration:run",
"migration:revert": "ts-node ./node_modules/typeorm/cli.js migration:revert",
},
"...",

}

Now from the terminal:

npm run migration:run

If everything was ok, you should see as last message:

Migration CreateMovieTable1584289576471 has been executed successfully.

Now we should have a new table in the database. If something went wrong during the migration, be sure to use the correct configurations in the ormconfig.json. To countercheck that the table is now part of the database you can use a database browser for SQLite, and navigate through your database.

To complete this phase, we should create the Movie model in our project. We just create a new file called Movie.model.ts inside the “models” folder.

## /db/models/Movie.model.tsimport {
BaseEntity,
Column,
Entity,
PrimaryGeneratedColumn,
} from "typeorm";
@Entity('movies')
export class Movie extends BaseEntity{
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
plot_summary: string;
@Column()
duration: number;
}

Now we’re ready to create some REST endpoints to work with movies!

CRUD for Movies

Create, Read, Update and Delete. These are the operations that characterize every resource in the REST design pattern. Each of them has also a correspondent HTTP Method and URL style following a convention. For example, in our case:

  • Create — POST /movies
  • Retrieve all — GET /movies
  • Retrieve single — GET /movies/:id
  • Update — PUT /movies/:id
  • Delete — DELETE /movies/:id

With “:id” we’re indicating the ID of the resource that we want that action performed on. Note that in the above notation I avoided including the hostname of our Express server for simplicity. For example, the full path for the Retrieve operation will be the following:

GET http://localhost:5000/movies

All of the four endpoints will be included in our app.ts.

The first endpoint that we will set is the one that will handle the Movie creation.

## ~/rotten-potatoes-api/src/app.tsimport express from 'express';
import * as bodyParser from 'body-parser';
import {Movie} from "./db/models/Movie.model";
const app = express();
app.use(bodyParser.json({
limit: '50mb',
verify(req: any, res, buf, encoding) {
req.rawBody = buf;
}
}));
app.get('/', (req, res) => res.send('Hello World!'));
app.post('/movies', async (req,res) => {
const movie = new Movie();
movie.title = req.body.title;
movie.plot_summary = req.body.plot_summary;
movie.duration = req.body.duration;
await movie.save();
res.send(movie);
});
export {app};

In this way, if we send a POST request to http://localhost:5000/movies endpoint, our code will:

  1. Initialize a new movie, using typeorm;
  2. Set the movie’s properties from the HTTP request body;
  3. Save the movie in the database;
  4. Return to the client the new movie that was just created.

You can simulate the HTTP call as before, using Postman or cURL from the terminal, being sure passing the correct fields in the body of the request.

NB: if you are not familiar with the async/await syntax, I suggest you to take a look at this article. Long story short, if you use await over a function that was declared async, your code will not go over until that function is completed.


Then we create the Read endpoint, to be sure that our movie was successfully inserted into the database with the correct properties. For the following endpoints, I will NOT include again the whole app.ts code, you just need to be sure to include them after the “app = express()” declaration.

## ~/rotten-potatoes-api/src/app.tsconst app = express();...app.get('/movies', async (req,res) => {
const movies = await Movie.find();
res.send(movies);
});
...export{app}

Note that the result of this HTTP call will give as response an array of movies.


Following with the CRUDs, we implement the Read function related to a specific movie.

## ~/rotten-potatoes-api/src/app.tsconst app = express();...app.get('/movies/:id', async (req,res) => {
const movie = await Movie.findOne({
where: {
id: req.params.id
}
});
if (movie){
res.json(movie);
} else {
res.status(404).send({message: "Movie not found"})
}
});
...export{app}

In this case, since we want to retrieve the data of a specific movie, we need to specify it in some ways. Thanks to express, we can manage to pass the ID of the movie that we want to retrieve inside the URL. The result is that can then be taken from the “req.params” object.

For example, if we call GET http://localhost:5000/movies/4, inside “req.params” we will have “4”.Then, using typeorm’s findOne function, we can search for that specific movie inside the database. In case the movie is not found, we return a 404 status code, which is the standard HTTP status for not found resources.


The update function as well will contain the ID parameter in the URL and it will use the HTTP PUT method. As for the POST, the PUT allows passing a body with the request. Of course, we will use the body to update the properties of our movie. What we’re doing in the code below is the following: we search for the movie with the specified ID and then if it is found, we update its field. Otherwise, we return a “Movie not found” message to the client.

## ~/rotten-potatoes-api/src/app.tsconst app = express();...app.put('/movies/:id', async (req,res) => {
const movie = await Movie.findOne({
where: {
id: req.params.id
}
});
if (movie){
movie.title = req.body.title;
movie.plot_summary = req.body.plot_summary;
movie.duration = req.body.duration;
await movie.save();
res.send(movie);
} else {
res.status(404).send({message: "Movie not found"})
}
});
...export{app}

Note that, with a code written like that, if you are not passing one of the movie’s fields in the body, that specific movie’s field will be set to null. A possible improvement could be something like this:

## ~/rotten-potatoes-api/src/app.tsconst app = express();...app.put('/movies/:id', async (req,res) => {
const movie = await Movie.findOne({
where: {
id: req.params.id
}
});
if (movie){
if (req.body.title) {
movie.title = req.body.title;
}
if (req.body.plot_summary){
movie.plot_summary = req.body.plot_summary;
}
if (req.body.duration){
movie.duration = req.body.duration;
}
await movie.save();
res.send(movie);
} else {
res.status(404).send({message: "Movie not found"})
}
});
...export{app}

In this way, the field gots updated only if it was sent in the body.


Last (and least :-) ), the Delete method, which as well it requires the parameter ID in the URL.

## ~/rotten-potatoes-api/src/app.tsconst app = express();...app.delete('/movies/:id', async (req,res) => {
const movie = await Movie.findOne({
where: {
id: req.params.id
}
});
if (movie){
await movie.remove();
res.json({message: 'Movie deleted'});
} else {
res.status(404).send({message: "Movie not found"})
}
});
...export{app}

And that’s it!

Conclusion

We’ve just created a fully functioning REST API that handles CRUD operations over the Movie resource. Of course, it can be extended in many different ways, to perform more complex operations. In the next episode of this series, I will rework this project to an even more structured codebase, having in mind the basic that we just see during this tutorial.

Thanks for your attention!

Source Code:

https://github.com/simonestaffa/how-to-rest-api-basic

The Startup

Medium's largest active publication, followed by +706K people. Follow to join our community.

Simone Staffa

Written by

MSc Computer Science & Engineering at Politecnico di Milano. Developer Student Club Lead at Politecnico di Milano. Backend Developer at Tutored.

The Startup

Medium's largest active publication, followed by +706K people. Follow to join our community.

Simone Staffa

Written by

MSc Computer Science & Engineering at Politecnico di Milano. Developer Student Club Lead at Politecnico di Milano. Backend Developer at Tutored.

The Startup

Medium's largest active publication, followed by +706K people. Follow to join our community.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store