Scaffolding Typescript API - Part 1

In the exciting world of web development, JavaScript has been the dominant programming language for a long time. Its flexibility and ease of use have made it the favorite choice of developers for creating interactive and dynamic web applications. However, as web applications become increasingly complex, the challenges associated with code maintenance and scalability also increase.
This is where TypeScript comes in, a superset of JavaScript that adds static typing features and additional characteristics to the language. Developed by Microsoft, TypeScript has gained popularity in recent years due to its ability to improve security and productivity in JavaScript application development. In this article, we will explore what TypeScript is and how it can benefit developers.
What is TypeScript?
TypeScript is often described as "statically typed JavaScript." Unlike JavaScript, where variable types are dynamically inferred at runtime, TypeScript allows developers to add static type annotations to their variables, functions, and objects. These type annotations provide additional information to the TypeScript compiler, allowing it to detect errors and potential problems during the compilation phase rather than at runtime.
Benefits of TypeScript:
-
Greater security: By adding static types, TypeScript helps detect common errors, such as invalid operations at compile time, incorrect use of variables, and calls to non-existent functions. This significantly reduces the possibility of runtime errors and makes code maintenance easier.
-
Better productivity: TypeScript offers advanced development tools, such as code autocompletion, quick navigation between files, and early error detection. These features improve developer productivity by providing contextual suggestions and real-time warnings, allowing them to write code more efficiently and quickly.
-
Scalability: As web applications grow in size and complexity, maintaining well-organized and scalable code becomes crucial. TypeScript offers object-oriented features and modules, making it easier to structure and maintain large-scale projects. Additionally, by allowing type checking, TypeScript facilitates code refactoring and the incorporation of new features without fear of breaking existing code.
-
JavaScript compatibility: TypeScript is a superset of JavaScript, which means that any existing JavaScript code is valid in TypeScript. This makes it easier to gradually adopt TypeScript in existing projects without the need to rewrite the code completely. Developers can start using TypeScript in specific parts of the project and gradually expand its use as needed.
Creating an API scaffold in TypeScript
In this project, we will dive into building a scaffold for an API using TypeScript and Express. As web applications continue to evolve, APIs have become the backbone of many modern solutions, enabling efficient and secure communication and data exchange between different systems.
We will begin by familiarizing ourselves with TypeScript, a superset of JavaScript that adds static typing features to the language. We will explore how to use TypeScript to improve the security and reliability of our API development, by allowing us to detect errors at compile time and maintain more robust code.
1. Create project in GIT
2. Create and configure .gitignore
.idea
.vscode
**/node_modules
dist
package-lock.json
3. Install TypeScript and nodemon as "dev" dependencies
npm i nodemon --save-dev
npm i typescript --save-dev
4. Configure TypeScript
Initialize TypeScript
npx tsc --init
Add the following configuration to the created file:
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"inlineSourceMap": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictPropertyInitialization": false
},
"include": [
"app/**/*.ts"
],
"exclude": [
"node_modules",
"test/**/*.ts"
]
}
5. Installation of Express and some necessary types
For Express installation, we will need the following packages:
- express
Winston is a middleware that will help us with the logs of our application:
- winston
- expressWinston
Helmet is recommended by Express as a good security practice for header validation. You can see it at: https://expressjs.com/en/advanced/best-practice-security.html#use-helmet
- helmet
- body-parser
npm install express winston express-winston helmet body-parser --save
We will also install the types of these packages to be able to use them with TypeScript:
npm install @types/express @types/body-parser --save-dev
6. Before configuring Express, we will need a simple controller.
We will create the folder where our entire project will be located, called "app".
We will create our controller, which will be a "Health" controller. It will only return the status of our API. In this case, it will only return the version of Express we are using. The controller will be located in the "app/controllers" folder and will be called "health.controller.ts" and will contain the following:
import express from "express";
const HTTP_STATUS_CODE_OK = 200;
const EXPRESS_VERSION = '4.17.17';
export class HealthController {
/**
* Get health of application
* @param req
* @param res
*/
async get(req: express.Request, res: express.Response) {
return res.status(HTTP_STATUS_CODE_OK).send({
expressVersion: EXPRESS_VERSION
})
}
}
7. We will need a router to manage the routes of our resources or modules.
We will create a folder called "app/routes/interfaces" and create an interface that defines 2 main functionalities. This interface must configure its routes, that is, register each of the routes for the indicated resource, adding the middlewares and indicating the handler, and get the name of the router.
In this case, interface segregation is not necessary, because in theory there would not be a router without a name or without routes. So, the file name will be: "router.interface.ts" and will contain the following code:
export interface Router {
/**
* Get name of router
* @return string
*/
get name(): string;
/**
* Configure routes for some controller
*/
configureRoutes(): void;
}
8. We will create an abstract class for a router
Inside "app/routes" called "abstract.router.ts" that will always return the name of the router following the Open/Closed principle.
import {Router} from "./interfaces/router.interface";
export abstract class AbstractRouter implements Router {
/**
* RouterName
* @private
*/
protected routerName: string;
configureRoutes(): void {
console.warn("It hasn't routes yet");
}
get name(): string {
return this.routerName;
}
}
9. It's time to create the router for our "Health"
Inside "app/routes". The file will be called "health.router.ts".
import express from "express";
import {HealthController} from "../controllers/health.controller";
import {Router} from "./interfaces/router.interface";
import {AbstractRouter} from "./abstract.router";
export class HealthRouter extends AbstractRouter implements Router {
/**
* @inheritDoc
*/
protected routerName: string = "HealthRouter";
/**
* @inheritDoc
*/
constructor(public app: express.Router) {
super();
this.configureRoutes();
}
/**
* @inheritDoc
*/
configureRoutes() {
let healthController = new HealthController();
this.app.get(`/health`, [
healthController.get,
]);
}
}
10. We will create a file to get our routes
Finally, to be able to read each of these routers and add them to the Express router, we will create a file that will be inside "app/routes" and will be called "index.ts".
import express, {Router as RouterExpress} from "express";
import {Router} from "./interfaces/router.interface";
import {HealthRouter} from "./health.router";
/**
* Configure all routes of API
* @param app
*/
export function setupRoutes(app: express.Application) {
const routes: any = [];
let router = RouterExpress();
routes.push(new HealthRouter(router));
app.use('/api/v1', router)
routes.forEach((route: Router) => {
console.log(`Routes configured for ${route.name}`);
});
}
11. It's time to create the Express configuration.
In this configuration, we will add Winston and Helmet, as well as Body Parser. We will create the file "server.config.ts" inside "app/config" with the following code:
import http from "http";
import * as bodyParser from "body-parser";
import * as expressWinston from "express-winston";
import express from "express";
import * as winston from "winston";
import {setupRoutes} from "../routes";
import helmet from "helmet";
/**
* Options for Winston
*/
function getOptionsLogger() {
return {
transports: [
new winston.transports.Console()
],
format: winston.format.combine(
winston.format.colorize(),
winston.format.json()
)
};
}
/**
* Whitelist logger
* @param app
*/
function setMiddlewareLogger(app: express.Application) {
let index = expressWinston.requestWhitelist.indexOf('headers');
if (index !== -1) expressWinston.requestWhitelist.splice(index, 1);
app.use(expressWinston.logger(getOptionsLogger()));
}
/**
* Error logger
* @param app
*/
function setMiddlewareErrorLogger(app: express.Application) {
app.use(expressWinston.errorLogger(getOptionsLogger()));
}
/**
* Cors config
* @param app
*/
function setCorsConfig(app: express.Application) {
app.use(helmet());
app.use(function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Methods', 'GET,HEAD,PUT,PATCH,POST,DELETE');
res.header('Access-Control-Expose-Headers', 'Content-Length');
res.header('Access-Control-Allow-Headers', req.header('Access-Control-Request-Headers'));
if (req.method === 'OPTIONS') {
return res.status(200).send();
} else {
return next();
}
});
}
export function initServer(app: express.Application) {
const server: http.Server = http.createServer(app);
const port = 3000;
// Load body parser
app.use(bodyParser.json({limit: '5mb'}));
// API logger
setMiddlewareLogger(app);
// cors configuration
setCorsConfig(app);
// Routes
setupRoutes(app);
// API routes error logger
setMiddlewareErrorLogger(app);
server.listen(port, () => {
console.log(`scaffold-api running at port ${port}`);
});
}
12. Index of our application
Our Express configuration is ready, however, we are missing a piece in this small puzzle: a file that helps us start our entire API. For that, we will create a file called "app.ts" inside the "app" folder, which will contain the following:
import express from "express";
import {initServer} from "./config/server.config";
const app: express.Application = express();
initServer(app)
export default app;
13. Package.json commands
To be able to transpile our code and execute it, we will add two configurations to the scripts of our package.json.
"dev": "nodemon -e ts --exec \"tsc -p ./tsconfig.json && node ./dist/app.js\"",
"prod": "tsc -p ./tsconfig.json && node ./dist/app.js",
Results
When executing the "npm run dev" command, we can see that our code is compiled and a folder called "dist" is created. Subsequently, our server is executed with Node.js. The structure of our project is as follows, ignoring "node_modules" and "dist".
.
├── app
│ ├── app.ts
│ ├── config
│ │ └── server.config.ts
│ ├── controllers
│ │ └── health.controller.ts
│ └── routes
│ ├── abstract.router.ts
│ ├── health.router.ts
│ ├── index.ts
│ └── interfaces
│ └── router.interface.ts
├── package.json
├── package-lock.json
├── README.md
└── tsconfig.json
The output of our console is as follows:
> scaffold-api-typescript@1.0.0 dev
> nodemon -e ts --exec "tsc -p ./tsconfig.json && node ./dist/app.js"
[nodemon] 2.0.22
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: ts
[nodemon] starting `tsc -p ./tsconfig.json && node ./dist/app.js`
Routes configured for HealthRouter
marketplace-api running at port 3000
When accessing "http://localhost:3000/api/v1/health" a JSON with the following content is displayed:
{"expressVersion":"4.17.17"}
Conclusion
In this case, the code required to configure Express was much larger than the target code of our API. However, thanks to the proposed structure, it is possible to further separate our logic from the infrastructure. This helps us scale, refactor, and reuse code in a simpler way. In other posts, I will show a way to standardize our responses and also to use a dependency container, keeping in mind the Liskov substitution and dependency inversion.
Branch of this post: https://github.com/gouh/scaffold-api-typescript/tree/config-express-typescript-health
Related posts

Redis Pub/Sub
Redis Pub/Sub, which stands for *Publish and Subscribe*, is a messaging mechanism that allows applications to communicate in real time through channels or topics. Redis, an in-memory database, provides this functionality efficiently.

AJAX
Asynchronous JavaScript + XML (AJAX) is not a technology in itself, it is a term that describes a new way of using several existing technologies together.