How to Create a RestFul Api with Express/Typescript/Webpack/Babel.
Hello err body today we are going to do what every else do xDDD no I promise this will be the post I wish I had found.

As there different IDEs out there most of the things I’ll do them on the terminal. and that’s the first thing we about to do let’s open that B
once you find the place where to start the project, run the following
mkdir -p rest-api && cd rest-api && mkdir src config && mkdir ./src/{controllers,services,utils,types,db} && touch .npmrc babel.config.js webpack.config.js
If you follow up then you might have something like

first create an index.ts
inside src
like this touch ./src/index.ts
then open the .npmrc
file and paste the following:
registry=http://registry.npmjs.org/
save-exact=true
We tell npm where to look for them dependencies as well as “Broh” we want the exact version, no funny sh*t.

time to install dependencies ova here, there are quite a few dont trip.
first prod deps ….. Almost forgot.

this are the versions I use oki, oki lets continue….
npm init -y && npm i @babel/runtime @babel/runtime-corejs3 bcrypt bluebird body-parser class-validator compression core-js cors csrf csurf es6-promise express express-limiter helmet http-status-codes jsonwebtoken morgan mysql rate-limiter-flexible redis validator winston
NOTE: I know I did not said anything about the package.json, but if you notice we created it on the command above.
Now devDependencies.
npm i -D @babel/cli @babel/core @babel/plugin-transform-modules-commonjs @babel/plugin-transform-runtime @babel/preset-typescript @babel/register @types/bcrypt @types/bluebird @types/body-parser @types/compression @types/cors @types/express @types/helmet @types/jsonwebtoken @types/morgan @types/mysql @types/node @types/redis @typescript-eslint/eslint-plugin @typescript-eslint/parser babel-loader dotenv dotenv-expand eslint eslint-config-airbnb eslint-config-airbnb-base eslint-config-airbnb-typescript eslint-config-jsdoc eslint-config-prettier eslint-config-standard-with-typescript eslint-plugin-eslint-comments eslint-plugin-import eslint-plugin-jsdoc eslint-plugin-jsx-a11y eslint-plugin-node eslint-plugin-prettier eslint-plugin-promise eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-standard file-loader husky nodemon npm-run-all prettier rimraf ts-loader ts-node tslib typescript webpack webpack-cli webpack-node-externals eslint-plugin-promise eslint-plugin-jsdoc eslint-config-jsdoc @babel/preset-env @babel/plugin-transform-modules-commonjs
next prettier, eslint and editorconfig
touch .eslintrc.js .prettierrc .editorconfig
open editorconfig and paste
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4
# Tab indentation (no size specified)
[Makefile]
indent_style = tab
it tells the IDEs how they show display the files. Prettier is next
{
"printWidth": 80,
"singleQuote": false,
"parser":"typescript",
"arrowParens": "avoid",
"endOfLine": "lf",
"bracketSpacing": true,
"quoteProps": "as-needed",
"semi": true,
"tabWidth": 4,
"useTabs": false,
"trailingComma": "es5"
}
eslint
"use strict";
const path = require("path");module.exports = {
root: true,
extends: [
"eslint:recommended",
"plugin:promise/recommended",
"eslint-config-jsdoc",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
env: {
commonjs: true,
es6: true,
node: true
},
globals: {
Atomics: "readonly",
SharedArrayBuffer: "readonly",
process: "readonly"
},
parser: "@typescript-eslint/parser",
parserOptions: {
project: path.join(__dirname, "./tsconfig.json"),
ecmaVersion: 2020,
sourceType: "module",
ecmaFeatures: {
impliedStrict: true,
jsx: false
}
},
plugins: ["@typescript-eslint", "jsdoc", "promise"],
settings: {
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"]
},
polyfills: ["promises"],
"import/resolver": {
node: {
moduleDirectory: "node_modules"
}
}
},
rules: {
"@typescript-eslint/unbound-method": 0,
"space-before-function-paren": ["error", "always"],
indent: ["error", 4],
quotes: ["error", "double"],
semi: ["error", "always"],
"keyword-spacing": [
"error",
{
before: true,
after: true
}
],
"arrow-body-style": ["error", "as-needed"],
"arrow-parens": ["error", "always"],
},
}
Something that I just want to mention ova here is that those files were made based on my personal preferences, please edit them to fulfil your needs.

Tow more extra config files and we is ready to throw some code around.
touch nodemon.json && ./node_modules/typescript/bin/tsc --init
open your nodemon.json and paste this.
{
"verbose": true,
"watch": ["src"],
"ext": "ts",
"exec": "ts-node ./src/index.ts",
"env": {
"NODE_ENV": "development",
"ENETO_DB_LIMIT":"10",
"ENETO_DB_HOST":"localhost",
"ENETO_DB_USER":"root",
"ENETO_DB_PASSWORD":"password",
"ENETO_DB_DATABASE":"rest-resume-api",
"ENETO_PORT":"5000",
"PORT": "5000",
"NODE_ENV": "development"
}
}
I mean the port and name of variables is up to you, oki (:3)
now open tsconfig.json
you can keep the comments if you like, just make sure yours looks like mine, or even better play with it to fill your needs. Mine looks like this!
{
"compilerOptions": {
"incremental": true,
"target": "es2020",
"module": "commonjs",
"lib": ["DOM", "ES2020"],
"allowJs": false,
"checkJs": false,
"jsx": "preserve",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist-tsc",
"rootDir": "./src",
"composite": true,
"tsBuildInfoFile": "./tsBuildInfoFile.json",
"noEmit": true,
"importHelpers": true,
"downlevelIteration": true,
"isolatedModules": true,
"strict": true,
"noImplicitAny": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"baseUrl": "./node_modules",
"paths": {
"*": [
"./",
"./@types/",
"../src/types/"
]
},
"typeRoots": ["./src/types/"],
"types": [
"node",
"express",
"body-parser",
"helmet",
"compression",
"winston",
"mysql"
],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"preserveSymlinks": true,
"allowUmdGlobalAccess": true,
"forceConsistentCasingInFileNames": true,
"pretty": true,
"traceResolution": true
},
"include": [
"./src/express.ts",
"./src/server.ts",
"./src/**/*.ts",
"./src/**/**/*.ts",
"./src/**/**/**/*.ts"
],
"exclude": [
"./node_modules" ,
"./src/index.ts"
]
}
Time to play with webpack, open your file
module.exports = {
entry: [],
output: {},
resolve: {},
module: {}
};
this is the gain monster, people often get scared of it as myself, but if you really look at it is none but a “simple” JSON, I won’t lie, the true power of webpack is beyond me, but never learn technologies sooner o later they get deprecated. something that might help you is, look at webpack as I look at it
module.exports = {
from: [],
where: {},
HowToDoIt:{}
}
oki lets begin with the from
here is where you tell webpack whats its start line and our entry point will live at ./src/index.ts
so we need to let him know that
const nodeExternals = require('webpack-node-externals');
const webpack = require("webpack");
const path = require('path');module.exports = {
target: 'node',
node: {
__filename: false,
__dirname: false
},
externals: [nodeExternals()],
entry: [
"./src/index.ts"
],
}
By default webpack runs for web, but this is not the case thats why we need to specify “node” on the target.
The “node” object:
true
: Provide a polyfill.'mock'
: Provide a mock that implements the expected interface but has little or no functionality.'empty'
: Provide an empty object.false
: Provide nothing. Code that expects this object may crash with aReferenceError
. Code that attempts to import the module usingrequire('modulename')
may trigger aCannot find module "modulename"
error.
The array “externals” excludes dependencies from the output bundles, as the production bundle will live on the same dir as node_modules, there is no need to include them. as they’ll be there.
and on the entry array we put some polyfils and our index.ts at the very bottom.
That’s the from now lets do the “where”
mode: NODE_ENV,
devtool: 'source-map',
output: {
filename: '[name]-bundle.js',
chunkFilename: '[name].chunk.js',
path: path.resolve(__dirname, './dist-webpack'),
publicPath: '/',
libraryTarget: 'commonjs2'
},
Here we tell it where to place the end bundle, the names for the files, and because we are bundling for node and not browsers. We need to specify the libraryTarget, and as currently node runs on commonjs modules lets said that.
This is the “HOW” part webpack by now knows from where hes gonna start, where to place the results, but he does know yet how to do it, so is our job to tell him. first
optimization: {
splitChunks: {
automaticNameDelimiter: '_',
cacheGroups: {
vendor: {
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
minChunks: 2
}
}
}
},
resolve: {
extensions: [".js", ".ts"]
},
we are going to separate the code there will be our code and the vendors code express/bodyParser bla bla bla oki :3, then as webpack only knows JavaScript, lets tell him the extensions to look for.
at the end lets put the plugins and loader.
module: {
rules: [
{
test: /\.(js|ts)$/,
exclude: /node_modules/,
include: /src/,
use: [
{
loader: 'babel-loader'
}
]
}
]
},
plugins: [
new webpack.DefinePlugin(stringified)
]
You might be thinking where that “stringified” thing came from, dont trip. we are going to do it rite now. first, our config should look like this
const nodeExternals = require('webpack-node-externals');
const webpack = require("webpack");
const path = require('path');module.exports = {
target: 'node',
node: {
__filename: false,
__dirname: false
},
externals: [nodeExternals()],
entry: "./src/index.ts",
mode: NODE_ENV,
devtool: 'source-map',
output: {
filename: '[name]-bundle.js',
chunkFilename: '[name].chunk.js',
path: path.resolve(__dirname, './dist-webpack'),
publicPath: '/',
libraryTarget: 'commonjs2'
},
optimization: {
splitChunks: {
automaticNameDelimiter: '_',
cacheGroups: {
vendor: {
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
minChunks: 2
}
}
}
},
resolve: {
extensions: [".js", ".ts"]
},
module: {
rules: [
{
test: /\.(js|ts)$/,
exclude: /node_modules/,
include: /src/,
use: [
{
loader: 'babel-loader'
}
]
}
]
},
plugins: [
new webpack.DefinePlugin(stringified)
]
};
now lets add the env variables to the webpack plugin we have at the bottom.
at the very top first lets add this
require('dotenv-expand')(
require('dotenv').config({
path: path.join(__dirname, ".env")
})
);
and above webpack and after the dependencies lets put this
const ENETO = /^ENETO_/i;
function vars () {
const raw = Object.keys(process.env)
.filter(key => ENETO.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];
return env;
},
{
NODE_ENV,
PUBLIC_URL: "/",
PORT: 5000
}
);
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, {})
};
return { raw, stringified };
}
remember when I told you the names of the variables depend on you, please give them a common prefix, mine is “ENETO”, first what this function does is.
Iterates on all the process.env variables and all variables that start with our common prefix will return them we stringify them. So the end product looks like this.
require('dotenv-expand')(
require('dotenv').config({
path: path.join(__dirname, ".env")
})
);
const nodeExternals = require('webpack-node-externals');
const webpack = require("webpack");
const path = require('path');
const { NODE_ENV } = process.env;
const ENETO = /^ENETO_/i;
function vars () {
const raw = Object.keys(process.env)
.filter(key => ENETO.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];
return env;
},
{
// if we want to add some of our on env variables do it here. NODE_ENV,
PUBLIC_URL: "/",
PORT: 5000
}
);
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, {})
};
return { raw, stringified };
}
const { stringified } = vars();
module.exports = {
target: 'node',
node: {
__filename: false,
__dirname: false
},
externals: [nodeExternals()],
entry: "./src/index.ts",
mode: NODE_ENV,
devtool: 'source-map',
output: {
filename: '[name]-bundle.js',
chunkFilename: '[name].chunk.js',
path: path.resolve(__dirname, './dist-webpack'),
publicPath: '/',
libraryTarget: 'commonjs2'
},
optimization: {
splitChunks: {
automaticNameDelimiter: '_',
cacheGroups: {
vendor: {
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
minChunks: 2
}
}
}
},
resolve: {
extensions: [".js", ".ts"]
},
module: {
rules: [
{
test: /\.(js|ts)$/,
exclude: /node_modules/,
include: /src/,
use: [
{
loader: 'babel-loader'
}
]
}
]
},
plugins: [
new webpack.DefinePlugin(stringified)
]
};
It’s time to start coding but first change your package.json scripts section with this
"scripts": {
"clean": "rimraf dist && rimraf dist-webpack && rimraf dist-babel && rimraf dist-tsc && rimraf reports && rimraf build && rimraf tsconfig.tsbuildinfo",
"lint": "npm run pretty && eslint -c ./.eslintrc.js ./src/*.ts ./src/**/*.ts ./src/**/**/*.ts ./src/**/**/**/*.ts ./src/**/**/**/**/*.ts --color --fix",
"pretty": "prettier ./dist/*.js ./src/*.ts ./src/**/*.ts ./src/**/**/*.ts ./src/**/**/**/*.ts ./src/**/**/**/**/*.ts --color --write",
"build:babel": "NODE_ENV=production babel --config-file ./babel.config.js ./src --extensions .ts --out-dir ./dist-babel",
"build:tsc": "tsc --build",
"build:webpack": "NODE_ENV=production webpack --config=webpack.config.js --mode=production --color",
"build": "npm run clean && run-p -l build:*",
"type-check": "tsc --traceResolution",
"start": "nodemon --inspect ./src/index.ts"
},
clean: removes the bundles folders.
lint: checks if there are some errors on our code.
pretty: make our code to look pretty.
build:babel: compiles our code with babel.
build:tsc: compiles our code with typescript.
build:webpack: compiles the code with webpack.
build: runs the tree commands above.
type-check: checks if we are following the rules.
start: well you know
While doing that I notice we have not do anything on babel.config.js lets do it.
module.exports = function (api) {
const {NODE_ENV} = process.env;
api.cache(() => NODE_ENV);
api.env();
return {
presets: [
["@babel/preset-env", {
targets: {
node: "current",
esmodules: false
},
useBuiltIns: "entry",
corejs: 3,
modules: "commonjs"
}],
"@babel/preset-typescript"
],
plugins: [
"@babel/plugin-transform-modules-commonjs",
"@babel/plugin-transform-runtime"
]
}
}
lets not get too fancy ova here as this code will be running with the latest version of node, so no need to add too much.
Now lets create the express file.
import { json, urlencoded } from "body-parser";
import compression from "compression";
import cors from "cors";
import express from "express";
import helmet from "helmet";
import morgan from "morgan";
import { routes } from "./routes";
import { logger } from "./utils/logger";
/**
* Creates an express instance.
*
* @param {string} env - environment in which the app will run.
* @returns {import("express").Application} express instance.
*/
export function start (env: string): express.Application {
logger.debug(`App running as ${env}`);
const app: express.Application = express();
if (env === "production") {
app.use(helmet());
app.disable("x-powered-by");
app.use(compression());
} else {
app.use(cors());
}
app.use(json());
app.use(urlencoded({ extended: true }));
app.use(morgan("dev"));
app.use("/api", routes());
return app;
}
Next the logger /src/utils/logger.ts
No matter what you should always put some goddamn loggs in yow sh*t.
import winston from "winston";
const {format} = winston;
const custom = {
levels: {
error: 0,
warn: 1,
info: 2,
verbose: 3,
debug: 4,
http: 5,
},
colors: {
error: "red",
warn: "orange",
info: "white bold yellow",
verbose: "blue",
debug: "green",
http: "pink",
},
};
const { NODE_ENV } = process.env;
winston.addColors(custom.colors);
export const myFormat = format.printf(
(info) => `[${info.timestamp}] [${info.level}] [38;5;13m[1m=>[22m[39m ${info.message}`
);
export const logger = winston.createLogger({
levels: custom.levels,
level: NODE_ENV === "production" ? "error" : "debug",
format: format.combine(
format.label({ label: "order-api errors" }),
format.timestamp(),
format.colorize({ colors: custom.colors }),
format.json(),
myFormat,
),
transports: [
new winston.transports.File({ filename: "info.log", level: "debug" }),
new winston.transports.File({ filename: "error.log", level: "error" }),
new winston.transports.Console({
level: NODE_ENV === "production" ? "error" : "debug",
}),
],
});
We have the logger, we have express file, now lets create the server. I mean if we is doing a rest api in includes a server, right?..
touch ./src/server.ts
there is where we are going to put the “stuff”

Obvi on not just going to put the image, here is the code:
import http from "http";
import { start } from "./express";
import { logger } from "./utils/logger";
const app = start(process.env.NODE_ENV || "development");
const port = process.env.PORT || 5000;
const server = http.createServer(app);
server.listen(Number(port), () => {
logger.info("server up and running");
logger.info(`at: http://localhost:${port}`);
logger.info(`as ${process.env.NODE_ENV}`);
});
But if you remember we tell webpack the entry point will be ./src/index.ts
that is next.

require("@babel/register");
require("@babel/runtime-corejs3/regenerator");
require("core-js/stable");
require("es6-promise/auto");
require("./server");
first babel to register and make use of the cache, next some runtime pulifys and at the very bottom ./erver.ts if we now run the following ts-node ./src/server.ts
our server will be alive.

see how mean you guys are, why didnt you tell me “hey my N, what about them routes”.
lets do them then, open your your routes.ts file and paste this… true that, we dont have a routes file. I’ll put it in ./src/utils/routes.ts
it does not matter.
import * as express from "express";
import { getStatusText, OK } from "http-status-codes";
export function routes (): express.Router {
const api: express.Router = express.Router();
api.get("/", (req: express.Request, res: express.Response) => {
return res.status(OK).send({success: true, message: getStatusText(OK)});
});
return api;
}
For now lets have that route there, now lets modify the express file.
so if you dont let me lie your file looks like so
import { json, urlencoded } from "body-parser";
import * as compression from "compression";
import * as cors from "cors";
import * as express from "express";
import * as helmet from "helmet";
import * as morgan from "morgan";
import { routes } from "./utils/routes";
import { logger } from "./utils/logger";
/**
* Creates an express instance.
*
* @param {string} env - environment in which the app will run.
* @returns {import("express").Application} express instance.
*/
export function start(env: string): express.Application {
logger.debug(`App running as ${env}`);
const app: express.Application = express();
if (env === "production") {
app.use(helmet());
app.disable("x-powered-by");
app.use(compression());
} else {
app.use(cors());
}
app.use(json());
app.use(urlencoded({ extended: true }));
app.use(morgan("dev"));
app.use("/api", routes());
return app;
}
Now if we run npm start
will get

Let’s see what happends if we open the browser at http://localhost:5000/api

we made it you guys. next time I’ll show you how to connect to a database it can be any kind. just please let me know which one. I hope this one was useful to you, because I wish I had come across something like this when I was learning and of course Im still are.
One last thing I cannot leave you with that ugly routes, lets do a constroller touch ./src/controllers/init-controller.ts
import {Response, Request} from "express";
import { CREATED, getStatusText } from "http-status-codes";
export class InitController {
public constructor () {
this.hello = this.hello.bind(this);
}
/**
* Prints a pretty message.
*
* @param {import("express").Request} req
* @param {import("express").Response} res
* @returns {Response}
*/
public hello (req:Request, res: Response): Response {
return res.status(CREATED).send({success: true, message: getStatusText(CREATED)})
}
}
time to change the routes.
import {Router} from "express";
import { InitController } from "../controllers/init-controller";
export function routes (): Router {
const api: Router = Router();
const init = new InitController();
api.get("/", init.hello);
return api;
}
oki. now is your turn till the next time.