NodeJS and TypeScript - up1/workshop-springboot-nodejs-go GitHub Wiki
- Express
- Validation data
- Manage database
Install TypeScript as Global
$npm install -g typescript
Create Project
$mkdir demo-rest-ts
$cd demo-rest-ts
$npm init -y
Install TypeScript in NodeJS
$npm install --save-dev typescript ts-node
Initial TypeScript
$tsc --init
Install express and express's type
$npm install -S express
$npm install -D @types/express
File server.ts
import express, { Request, Response } from "express";
const app = express();
const port = 3000;
app.get("/", (req: Request, res: Response) => {
res.send("Hello World!");
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
Run
$tcs
$node dist/server.js
Config file package.json
"scripts": {
"dev": "ts-node server.ts",
"start": "rm -rf ./dist && tsc --outDir ./dist && node ./dist/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
}
Or run with dev mode
$npm run dev
Or run with production mode
$npm run start
import express, { Application, Request, Response } from "express";
class Server {
private app: Application;
private port: number;
constructor(port: number = 3000) {
this.app = express();
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
this.port = port;
this.setupRoutes();
}
private setupRoutes(): void {
this.app.get("/", this.handleRoot.bind(this));
}
private handleRoot(req: Request, res: Response): void {
res.status(200).json({ message: "Hello World!" });
}
public start(): void {
this.app.listen(this.port, () => {
console.log(`Example app listening on port ${this.port}`);
});
}
}
// Create and start the server
const server = new Server();
server.start();
- Get product by id
- Create a new product
File product.ts
import { Application, Request, Response } from "express";
export class Product {
private app: Application;
constructor(app: Application) {
this.app = app;
this.setupRoutes();
}
private setupRoutes(): void {
this.app.get("/product/:id", this.getProductById.bind(this));
this.app.post("/product", this.createProduct.bind(this));
}
private createProduct(req: Request, res: Response): void {
const newProduct = req.body;
res.status(201).json({
message: "Product created successfully",
product: newProduct
});
}
private getProductById(req: Request, res: Response): void {
const productId = req.params.id;
res.status(200).json({
id: productId,
name: "Sample Product",
price: 100.50,
description: "This is a sample product description."
});
}
}
Add product to File server.ts
...
constructor(port = 3000) {
this.app = express();
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
this.port = port;
this.setupRoutes();
new Product(this.app);
}
...
- Jest
- SuperTest
Install
$npm install --save-dev jest supertest @types/jest @types/supertest ts-jest
Create file jest.config.js
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testMatch: ["**/*.test.ts"],
collectCoverage: true,
coverageDirectory: "coverage",
collectCoverageFrom: ["**/*.ts", "!**/*.d.ts"],
};
Edit file package.json
"scripts": {
...
"test": "jest"
}
Run test
$npm test
Create test case in file __tests__/product.test.ts
import request from 'supertest';
import express, { Application } from 'express';
import { Product } from '../product';
describe('Express API', () => {
let app: Application;
beforeEach(() => {
// Create a fresh express app for each test
app = express();
app.use(express.json()); // Add middleware to parse JSON bodies
// Set up routes directly instead of using Server class to avoid actual server listening
app.get('/', (req, res) => {
res.status(200).json({ message: 'Hello World!' });
});
// Initialize Product routes
new Product(app);
});
describe('Root endpoint', () => {
it('should return Hello World message', async () => {
const response = await request(app).get('/');
expect(response.status).toBe(200);
expect(response.body).toEqual({ message: 'Hello World!' });
});
});
describe('Product endpoints', () => {
it('should get a product by ID', async () => {
const productId = '123';
const response = await request(app).get(`/product/${productId}`);
expect(response.status).toBe(200);
expect(response.body).toEqual({
id: productId,
name: 'Sample Product',
price: 100.50,
description: 'This is a sample product description.'
});
});
it('should create a new product', async () => {
const newProduct = {
name: 'New Product',
price: 199.99,
description: 'A brand new product'
};
const response = await request(app)
.post('/product')
.send(newProduct)
.set('Content-Type', 'application/json');
expect(response.status).toBe(201);
expect(response.body).toEqual({
message: 'Product created successfully',
product: newProduct
});
});
});
});
$npm install joi
$npm install pg sequelize-typescript
$npm install sequelize reflect-metadata sequelize-typescript
$npm install --save-dev @types/pg
$npm install --save-dev @types/node @types/validator
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USERNAME=user01
POSTGRES_PASSWORD=password01
POSTGRES_DATABASE_NAME=demodb
REDIS_HOST=localhost
REDIS_PORT=6379
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
// Working with postgresql and sequelize
import { Sequelize } from "sequelize-typescript";
import { MyProduct } from "./models/product_model";
export class Database {
private static instance: Database;
private sequelize: Sequelize;
private constructor() {
this.sequelize = new Sequelize({
database: process.env.POSTGRES_DATABASE_NAME,
dialect: "postgres",
host: process.env.POSTGRES_HOST,
password: process.env.POSTGRES_PASSWORD,
username: process.env.POSTGRES_USERNAME,
models: [MyProduct], // your models
});
this.sequelize.sync({ force: true });
}
public static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
public getSequelize(): Sequelize {
return this.sequelize;
}
}
import { Table, Column, Model } from 'sequelize-typescript';
@Table({
tableName: 'demo_product',
})
export class MyProduct extends Model {
@Column({
type: 'varchar',
allowNull: false,
})
name!: string;
@Column({
type: 'varchar',
allowNull: true,
})
code?: string;
@Column({
type: 'integer',
allowNull: false,
})
quantity!: number;
@Column({
type: 'float',
allowNull: false,
})
price!: number;
}
import { Application, Request, Response } from "express";
import Joi from "joi";
import { Database } from "./database";
import { MyProduct } from "./models/product_model";
export class Product {
private app: Application;
private database: Database;
constructor(app: Application) {
this.app = app;
this.database = Database.getInstance();
this.setupRoutes();
}
private setupRoutes(): void {
this.app.get("/product/:id", this.getProductById.bind(this));
this.app.post("/product", this.createProduct.bind(this));
}
private async createProduct(req: Request, res: Response): Promise<void> {
const newProduct = req.body;
// Check if the request body has the correct data types
if (!req.is('application/json')) {
res.status(400).json({ error: 'Invalid content type' });
return;
}
// Validate the request body with joi
const productSchema = Joi.object({
name: Joi.string().required(),
code: Joi.string().required(),
price: Joi.number().required(),
quantity: Joi.number().required(),
});
const { error } = productSchema.validate(newProduct);
if (error) {
console.error("Validation error:", error.details[0].message);
res.status(400).json({
message: "Validation error",
error: error.details[0].message,
});
return;
}
console.log("Creating product:", newProduct);
const product = new MyProduct({
name: newProduct.name,
code: newProduct.code,
price: newProduct.price,
quantity: newProduct.quantity,
});
try {
const savedProduct = await product.save();
console.log("Product created successfully.");
res.status(201).json({
id: savedProduct.id,
name: savedProduct.name,
code: savedProduct.code,
price: savedProduct.price,
quantity: savedProduct.quantity,
});
} catch (error: any) {
console.error("Error creating product:", error);
res.status(500).json({
message: "Error creating product",
error: error.message,
});
}
}
private async getProductById(req: Request, res: Response): Promise<void> {
const productId = req.params.id;
const product = await MyProduct.findByPk(productId);
if (!product) {
console.log("Product not found");
res.status(404).json({ message: "Product not found" });
return;
}
console.log("Product found:", product);
res.status(200).json({
id: product.id,
name: product.name,
price: product.price,
quantity: product.quantity,
});
}
}
7. Add ESLint
$npm install --save-dev eslint @eslint/js typescript-eslint
Create file eslint.config.mjs
// @ts-check
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.recommended,
);
Custom rules
// @ts-check
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.strict,
tseslint.configs.stylistic,
// add any additional rules or overrides here
{
rules: {
'no-console': 'warn',
'no-unused-vars': 'warn',
'quotes': ['error', 'single'],
'semi': ['error', 'always'],
// Add more rules as needed
},
}
);
Edit file package.json
"lint": "eslint '**/*.ts'"
Run
$npm run lint
Use pino-http with express
$npm install pino-http
Edit file server.ts
import express, { Application, Request, Response } from "express";
import { Product } from "./product";
import logger from "pino-http";
export class Server {
private app: Application;
private port: number;
constructor(port: number = 3000) {
this.app = express();
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
this.app.use(logger());
this.port = port;
this.setupRoutes();
}
...
private handleRoot(req: Request, res: Response): void {
req.log.info({key: "value"}, "Demo structured log");
res.status(200).json({ message: "Hello World!" });
}
...
}