Node Express Typescript 2024
Contents
Setup.
terminal :
npm init -y npm i --save express dotenv npm i -D typescript @types/node @types/express nodemon ts-node npx tsc --init
tsconfig.json
"rootDir": "./src", "outDir": "./dist",
package.json ->
{
"name": "logicmade_api",
"version": "1.0.0",
"description": "API for many things needed",
"main": "index.js",
"scripts": {
"build": "npx tsc",
"start": "node dist/index.js",
"dev": "nodemon src/index.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.11.28",
"nodemon": "^3.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.2"
},
"dependencies": {
"dotenv": "^16.4.5",
"express": "^4.18.3"
}
}
.env file
PORT=3000
./src/index.ts ->
import express, {Express, Request, Response } from "express"
import dotenv from "dotenv"
dotenv.config()
const app: Express = express()
const port = 3000
console.log("env : ", process.env.PORT )
app.get("/", (req:Request, res:Response) => {
res.send("OK")
})
app.listen(port, ()=> {
console.log(`[server]: Server is running at http://localhost:${port}`)
})
Vitest
npm i -D vitest npm i -D @vitest/ui
package.json ->
"scripts": {
...
"test": "vitest",
"test:ui": "vitest --ui",
"coverage": "vitest run --coverage"
},
Morgan (Logging the requests)
npm i -S morgan npm i -D @types/morgan npm i rotating-file-stream
/src/index.ts
import morgan from "morgan"
import { createStream } from "rotating-file-stream"
import path from "path"
...
import Router from "./routes"
...
var accessLogStream = createStream('access.log', {
interval: '1d', // rotate daily
path: path.join(__dirname, '../log')
})
...
app.use(express.json());
app.use(morgan('combined', { stream: accessLogStream }));
app.use(express.static("public"));
app.use(Router);
/src/routes/index.ts
import express from "express";
import PingController from "../controllers/ping";
const router = express.Router();
router.get("/ping", async (_req, res) => {
const controller = new PingController();
const response = await controller.getMessage();
return res.send(response);
});
export default router;
/src/controller/ping.ts
interface PingResponse {
message: string;
}
export default class PingController {
public async getMessage(): Promise<PingResponse> {
return {
message: "hello",
};
}
}
Swagger
npm i -S tsoa swagger-ui-express npm i -D @types/swagger-ui-express concurrently
tsconfig.json
{
"compilerOptions": {
...
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
tsoa.json (create)
{
"entryFile": "src/index.ts",
"noImplicitAdditionalProperties": "throw-on-extras",
"spec": {
"outputDirectory": "public",
"specVersion": 3
}
}
If bearer token will be used then json should be like below.
{
"entryFile": "src/index.ts",
"noImplicitAdditionalProperties": "throw-on-extras",
"spec": {
"outputDirectory": "public",
"specVersion": 3,
"securityDefinitions": {
"BearerAuth": {
"type": "http",
"scheme": "bearer"
}
}
},
"ignore": ["**/node_modules/**"]
}
For other security options see tsoa documentation. Some other samples are below
"securityDefinitions": {
"api_key": {
"type": "apiKey",
"name": "access_token",
"in": "query"
},
"tsoa_auth": {
"type": "oauth2",
"authorizationUrl": "http://swagger.io/api/oauth/dialog",
"flow": "implicit",
"scopes": {
"write:pets": "modify things",
"read:pets": "read things"
}
}
},
package.json
"nodemonConfig": {
"watch": [
"src"
],
"ext": "ts",
"exec": "ts-node src/index.ts"
},
"scripts": {
"predev": "npm run swagger",
"prebuild": "npm run swagger",
"build": "npx tsc",
"start": "node dist/index.js",
"dev": "concurrently \"nodemon\" \"nodemon -x tsoa spec\"",
"swagger": "tsoa spec",
...
},
Usage of Concurrently : concurrently 'command1 arg' 'command2 arg'
src/index.ts
import express, { Application, Request, Response } from "express";
import morgan from "morgan";
import swaggerUi from "swagger-ui-express";
import Router from "./routes";
const PORT = process.env.PORT || 8000;
const app: Application = express();
app.use(express.json());
app.use(morgan("tiny"));
app.use(express.static("public"));
app.use(
"/docs",
swaggerUi.serve,
swaggerUi.setup(undefined, {
swaggerOptions: {
url: "/swagger.json",
},
})
);
app.use(Router);
src/controllers/ping.ts
import { Get, Route } from "tsoa";
interface PingResponse {
message: string;
}
@Route("ping")
export default class PingController {
@Get("/")
public async getMessage(): Promise<PingResponse> {
return {
message: "hello",
};
}
}
After making all the changes and running the server, visit http://localhost:3000/docs/ to access the APIs documentation.
Drizzle (left uncompleted)
Depending on the database select packages to install by checking Drizzle docs
Neon
Neon provides postgres db in the cloud. Check : neon official web site
Drizzle setup
npm i drizzle-orm @neondatabase/serverless npm i -D drizzle-kit
/db/db.ts (different from original doc because of type error)
import { neon } from '@neondatabase/serverless';
import type { NeonQueryFunction } from "@neondatabase/serverless";
import { drizzle } from 'drizzle-orm/neon-http';
const sql: NeonQueryFunction<boolean, boolean> = neon(process.env.DRIZZLE_DATABASE_URL!);
const db = drizzle(sql);
export default db;
I couldn't find automatic schema updates in drizzle and swithing back to TypeORM
TypeORM
npm i typeorm pg
/src/config/config.ts
const config = {
db:{
sync:true,
migration:true
}
}
export default config
/src/db/connection.ts
import { DataSourceOptions } from "typeorm";
import dotenv from "dotenv"
dotenv.config()
const config : DataSourceOptions = {
type: "postgres",
host: process.env.DB_HOST,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME ,
entities:[
"src/db/entities/**/*.ts"
],
migrations: [
"src/db/migrations/**/*.ts"
],
synchronize: true,
logging: false,
migrationsRun: false,
extra: {
ssl: true
}
}
export default config
extra is needed for neon postgres sql
/src/db/dbSource.ts
import {DataSource} from "typeorm";
import connection from './connection'
import config from "../config/config"
const DbSource = new DataSource(connection)
DbSource.initialize()
.then(() => {
console.log("Database connected - " + connection.database)
if(config.db.sync?.toString()=="true"){
DbSource.synchronize().then(() => {
console.log("Database syncronized")
if(config.db.migration?.toString()=="true"){
DbSource.runMigrations().then(()=>{
console.log("Database migrations run")
})
}
})
}
}).catch(err => {
console.log("Unable to connect to db", err);
process.exit(1)
})
export default DbSource
/src/db/entities/user.entity.ts
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";
@Entity("logicUser")
export class User {
@PrimaryGeneratedColumn("uuid")
id!: string;
@Column()
fullname!: string;
@Column({nullable:true})
description!: string;
@Column({nullable:true})
email!: string;
}
There is a similar model class
/src/repositories/user.repository.ts
import crypto from 'crypto'
import DbSource from '../db/dbSource';
import { User } from '../db/entities/user.entity';
import { RetVal } from '../models/retval.model';
import { UserModel } from '../models/user.model';
export const getUser = async (id:string) : Promise<RetVal> => {
const userRepository = DbSource.getRepository(User);
try{
const result = await userRepository.findOne({
where:{
id:id
},
})
return new RetVal(null, result)
}catch{
return new RetVal(Error("No record found for given id"), null)
}
}
export const getUsers = async () : Promise<RetVal> => {
const userRepository = DbSource.getRepository(User);
try{
const result = await userRepository.find({
order:{fullname:"ASC"}
})
return new RetVal(null, result)
}catch{
return new RetVal(Error("No record found for given id"), null)
}
}
export const addUser = async (model : UserModel) : Promise<RetVal> => {
const userRepository = DbSource.getRepository(User);
const userEntity = userRepository.create({
id:crypto.randomUUID(),
fullname:model.fullname,
description:model.description,
email:model.email
})
try{
const result = await userRepository.save(userEntity);
return new RetVal(null, result)
}catch(error){
let message;
if (error instanceof Error) message = "Error : " + error.message
return new RetVal(Error("Error saving company"), null)
}
}