Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CRUD_BASE_URL=http://localhost
CRUD_US_PORT=4000
CRUD_DB_HOST=localhost
CRUD_DB_PORT=4500
42 changes: 42 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "nodejs-crud-api",
"version": "1.0.0",
"description": "Implementation of simple CRUD API using in-memory database",
"main": "src/index.ts",
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"start:dev": "npm exec nodemon src/single_mode.ts",
"start:prod": "npx webpack --mode production",
"start:multi": "npm exec nodemon src/index.ts"
},
"author": "docroot",
"license": "ISC",
"dependencies": {
"dotenv": "^16.3.1",
"ts-node": "^10.9.1",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/uuid": "^9.0.2",
"eslint": "^8.44.0",
"nodemon": "^1.19.4",
"ts-loader": "^9.4.4",
"ts-node-dev": "^2.0.0",
"typescript": "^5.1.6",
"webpack": "^5.88.1",
"webpack-cli": "^5.1.4"
},
"compilerOptions": {
"outDir": "./dist",
"module": "es6",
"target": "es5",
"jsx": "react-jsx",
"lib": [
"es6",
"dom"
],
"sourceMap": true
}
}
235 changes: 235 additions & 0 deletions src/db_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import http, { IncomingMessage, ServerResponse } from 'http';
import { parse } from 'url';
import * as uuid from 'uuid';
import { User } from './entities/User'


export class DbService {
private users: User[];
private dbPort: string;

constructor() {
this.users = [];
this.dbPort = process.env['CRUD_DB_PORT'] ? process.env['CRUD_DB_PORT'] : '4100';
}

createUser(user: User) {
this.users.push(user);
}

getUsers() {
return this.users;
}

getUserById(id: string) {
return this.users.find((user) => user.id === id);
}

updateUser(id: string, updatedUser: User) {
const index = this.users.findIndex((user) => user.id === id);
if (index !== -1) {
this.users[index] = updatedUser;
return this.users[index];
}
return false;
}

deleteUser(id: string) {
const index = this.users.findIndex((user) => user.id === id);
if (index !== -1) {
this.users.splice(index, 1);
return true;
}
return false;
}

start() {
const server = http.createServer((req: IncomingMessage, res: ServerResponse) => {
const { method, url } = req;

const parsedUrl = parse(url || '', true);
const path = parsedUrl.pathname || '';

let requestBody = '';

req.on('data', (chunk) => {
requestBody += chunk;
});

req.on('end', () => {
let response;
res.setHeader('Content-Type', 'application/json');

if (method === 'POST' && (path === '/api' || path === '')) {
try {
res.statusCode = 200;
const request = JSON.parse(requestBody);
// console.log(request);
if (request.cmd) {
const cmd = request.cmd;
// console.log(`CMD is [${cmd}]`);
const data = request.data;
switch (cmd) {
case 'USERS':
response = JSON.stringify(this.getUsers());
break;
case 'USER':
if (!data.id) {
res.statusCode = 400;
response = 'No user ID';
throw new Error(response);
}
if (!uuid.validate(data.id)) {
res.statusCode = 400;
response = 'User ID is not valid';
throw new Error(response);
}
// console.log(`User ID: [${data.id}]`);
const user = this.getUserById(data.id);
if (user) {
response = JSON.stringify(user);
// console.log(user);
}
else {
res.statusCode = 404;
response = 'User not found';
throw new Error(response);
}
break;
case 'ADD':
// console.log('ADD' + data);
if (!data.username || data.username === '' || !data.age) {
res.statusCode = 400;
response = 'User data is not valid';
throw new Error(response);
}
else {
const hobbies = Array.isArray(data.hobbies) ? data.hobbies : [];
const user = new User(uuid.v4(), data.username, data.age, hobbies);
this.createUser(user);
response = JSON.stringify(user);
res.statusCode = 201;
}
break;
case 'UPDATE':
// console.log('UPDATE' + data);
if (!data.id || !data.username || data.username === '' || !data.age) {
res.statusCode = 400;
response = 'User data is not valid';
throw new Error(response);
}
if (!uuid.validate(data.id)) {
res.statusCode = 400;
response = 'User ID is not valid';
throw new Error(response);
}
else {
const hobbies = Array.isArray(data.hobbies) ? data.hobbies : [];
const user = new User(data.id, data.username, data.age, hobbies);
const result = this.updateUser(data.id, user);
if (result) {
response = JSON.stringify(result);
}
else {
res.statusCode = 404;
response = 'User not found';
throw new Error(response);
}
}
break;
case 'DELETE':
// console.log('DELETE' + data);
if (!data.id) {
res.statusCode = 400;
response = 'No user ID';
throw new Error(response);
}
if (!uuid.validate(data.id)) {
res.statusCode = 400;
response = 'User ID is not valid';
throw new Error(response);
}
// console.log(`User ID: [${data.id}]`);
const result = this.deleteUser(data.id);
if (result) {
res.statusCode = 204;
response = 'DELETED';
}
else {
res.statusCode = 404;
response = 'User not found';
throw new Error(response);
}
break;
default:
throw new Error(`Unknown command ${cmd}`);
}
}
}
catch (error) {
// console.log("Incorrect request");
// console.log(error);
// console.log(response);
if (res.statusCode === 200) res.statusCode = 400;
}
// response = userService.getUsers();
// res.statusCode = 200;
} else {
response = { error: 'Not found' };
res.statusCode = 404;
}

res.end(response);
});
});


server.listen(this.dbPort, () => {
console.log(`DB service is running on port ${this.dbPort}`);
});
}


createTestUsers() {
const json = [{
id: uuid.v4(),
username: 'John',
age: 25,
hobbies: ['Ski', 'Videogames'],
},
{
id: 'f8561522-0681-41b4-979b-3b1ef3ae09de',//uuid.v4(),
username: 'Emma',
age: 27,
hobbies: ['Writing', 'Gardening'],
},
{
id: uuid.v4(),
username: 'Ben',
age: 52,
hobbies: ['Reading', 'Gardening'],
}
];

json.forEach((element) => {
const user: User = User.fromJson(element);
this.createUser(user);
});
}
}


// process.on('SIGTERM', () => {
// process.exit(0);
// });

// process.on('exit', () => {
// server.closeAllConnections();
// console.log("DB service done its work.");
// });


// process.on('error', (error) => {
// console.log(error);
// process.exit(0);
// });
18 changes: 18 additions & 0 deletions src/entities/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export class User {
id: string;
username: string;
age: number;
hobbies: string[];

constructor(uuid: string, username: string, age: number, hobbies: string[]) {
this.id = uuid;
this.username = username;
this.age = age;
this.hobbies = hobbies;
}

static fromJson(json: any): User {
const { id, username, age, hobbies } = json;
return new User(id, username, age, hobbies);
}
};
83 changes: 83 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import cluster, { Worker } from 'cluster';
import http, { IncomingMessage, ServerResponse } from 'http';
import { availableParallelism } from 'node:os';
import process from 'process';
import child_process, { spawn, ChildProcess } from 'child_process';
import dotenv from 'dotenv';
import { DbService } from "./db_service";
import { UserService } from "./user_service";

dotenv.config();

// Object.keys(process.env).forEach((key) => {
// if (key.startsWith('CRUD'))
// console.log(`${key}=[${process.env[key]}]`);
// });

const numCPUs = availableParallelism() - 1;
let activeChildProcesses = 0;
const masterPort = process.env['CRUD_US_PORT'] ? process.env['CRUD_US_PORT'] : '4000';
let currentWorkerPort = parseInt(masterPort) + 1;

if (cluster.isPrimary) {
console.log(`Primary ${process.pid} is running`);

const dbService = new DbService();
// dbService.createTestUsers();
dbService.start();


for (let i = 0; i < numCPUs; i++) {
cluster.fork({ 'CRUD_US_PORT': currentWorkerPort });
currentWorkerPort++;
}

cluster.on('exit', (worker: Worker, code: number, signal: string) => {
console.log(`worker ${worker.process.pid} exited with code ${code} and signal ${signal}`);
});

cluster.on('listening', (worker, address) => {
console.log(
`A worker is now connected to ${address.address}:${address.port}`);
});

const loadBalancer = http.createServer((req: IncomingMessage, res: ServerResponse) => {
const options = {
hostname: 'localhost',
port: 4001,
path: req.url,
method: req.method,
headers: req.headers
};

const outgoingReq = http.request(options, (outgoingRes) => {

if (outgoingRes && outgoingRes.statusCode) {
res.writeHead(outgoingRes.statusCode, outgoingRes.headers);
outgoingRes.pipe(res);
}
else {
res.statusCode = 500;
res.end('Error: Could not resend request');
}
});

outgoingReq.on('error', (error) => {
console.error(error);
res.statusCode = 500;
res.end('Error: Could not resend request');
});

req.pipe(outgoingReq);
}).listen(masterPort);

} else if (cluster.isWorker) {

console.log(`Cur port: [${process.env.CRUD_US_PORT}]`);
if (cluster.worker) console.log(`Worker ID: [${cluster.worker.id}]`);

const userService = new UserService();
userService.start();

console.log(`Worker ${process.pid} started`);
}
Loading