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
1 change: 1 addition & 0 deletions .env.examle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PORT=4000
11 changes: 11 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"parser": "@typescript-eslint/parser",
"extends":[
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"ecmaVersion": 5,
"sourceType": "module"
},
"rules": {}
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
dist/
.env
yarn.lock
8 changes: 8 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 120,
"useTabs": true,
"tabWidth": 2
}
74 changes: 18 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,65 +1,27 @@
# NodeJS_Task-3_Simple-CRUD-API
Task is to implement simple CRUD API using in-memory database underneath.

# Assignment: CRUD API
## Get started

## Description
`npm install` or `yarn install`

Your task is to implement simple CRUD API using in-memory database underneath.
## Rename file ".env.example" for ".env"

## Technical requirements
## Run in production mode

- Task can be implemented on Javascript or Typescript
- Only `nodemon`, `dotenv`, `cross-env`, `typescript`, `ts-node`, `eslint` and its plugins, `webpack-cli`, `webpack` and its plugins, `prettier`, `uuid`, `@types/*` as well as libraries used for testing are allowed
- Use 18 LTS version of Node.js
- Prefer asynchronous API whenever possible
`npm run start:prod` or `yarn start:prod`

## Implementation details
## Run in development mode

1. Implemented endpoint `api/users`:
- **GET** `api/users` is used to get all persons
- Server should answer with `status code` **200** and all users records
- **GET** `api/users/${userId}`
- Server should answer with `status code` **200** and and record with `id === userId` if it exists
- Server should answer with `status code` **400** and corresponding message if `userId` is invalid (not `uuid`)
- Server should answer with `status code` **404** and corresponding message if record with `id === userId` doesn't exist
- **POST** `api/users` is used to create record about new user and store it in database
- Server should answer with `status code` **201** and newly created record
- Server should answer with `status code` **400** and corresponding message if request `body` does not contain **required** fields
- **PUT** `api/users/{userId}` is used to update existing user
- Server should answer with` status code` **200** and updated record
- Server should answer with` status code` **400** and corresponding message if `userId` is invalid (not `uuid`)
- Server should answer with` status code` **404** and corresponding message if record with `id === userId` doesn't exist
- **DELETE** `api/users/${userId}` is used to delete existing user from database
- Server should answer with `status code` **204** if the record is found and deleted
- Server should answer with `status code` **400** and corresponding message if `userId` is invalid (not `uuid`)
- Server should answer with `status code` **404** and corresponding message if record with `id === userId` doesn't exist
2. Users are stored as `objects` that have following properties:
- `id` — unique identifier (`string`, `uuid`) generated on server side
- `username` — user's name (`string`, **required**)
- `age` — user's age (`number`, **required**)
- `hobbies` — user's hobbies (`array` of `strings` or empty `array`, **required**)
3. Requests to non-existing endpoints (e.g. `some-non/existing/resource`) should be handled (server should answer with `status code` **404** and corresponding human-friendly message)
4. Errors on the server side that occur during the processing of a request should be handled and processed correctly (server should answer with `status code` **500** and corresponding human-friendly message)
5. Value of `port` on which application is running should be stored in `.env` file
6. There should be 2 modes of running application (**development** and **production**):
- The application is run in development mode using `nodemon` (there is a `npm` script `start:dev`)
- The application is run in production mode (there is a `npm` script `start:prod` that starts the build process and then runs the bundled file)
7. There could be some tests for API (not less than **3** scenarios). Example of test scenario:
1. Get all records with a `GET` `api/users` request (an empty array is expected)
2. A new object is created by a `POST` `api/users` request (a response containing newly created record is expected)
3. With a `GET` `api/user/{userId}` request, we try to get the created record by its `id` (the created record is expected)
4. We try to update the created record with a `PUT` `api/users/{userId}`request (a response is expected containing an updated object with the same `id`)
5. With a `DELETE` `api/users/{userId}` request, we delete the created object by `id` (confirmation of successful deletion is expected)
6. With a `GET` `api/users/{userId}` request, we are trying to get a deleted object by `id` (expected answer is that there is no such object)
8. There could be implemented horizontal scaling for application (there is a `npm` script `start:multi` that starts multiple instances of your application using the Node.js `Cluster` API (equal to the number of logical processor cores on the host machine, each listening on port PORT + n) with a **load balancer** that distributes requests across them (using Round-robin algorithm). For example: host machine has 4 cores, `PORT` is 4000. On run `npm run start:multi` it works following way
- On `localhost:4000/api` load balancer is listening for requests
- On `localhost:4001/api`, `localhost:4002/api`, `localhost:4003/api`, `localhost:4004/api` workers are listening for requests from load balancer
- When user sends request to `localhost:4000/api`, load balancer sends this request to `localhost:4001/api`, next user request is sent to `localhost:4002/api` and so on.
- After sending request to `localhost:4004/api` load balancer starts from the first worker again (sends request to `localhost:4001/api`)
- State of db should be consistent between different workers, for example:
1. First `POST` request addressed to `localhost:4001/api` creates user
2. Second `GET` request addressed to `localhost:4002/api` should return created user
3. Third `DELETE` request addressed to `localhost:4003/api` deletes created user
4. Fourth `GET` request addressed to `localhost:4004/api` should return **404** status code for created user
`npm run start:dev` or `yarn start:dev`

## Starts multiple instances

`npm run start:multi` or `yarn start:multi`

## Start tests (Please follow the menu!!!)

`npm run test` or `yarn test`

### 1) press key "a",
### 2) after testing is finished, press the key to exit "w" --> "q"

65 changes: 65 additions & 0 deletions assignment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# NodeJS_Task-3_Simple-CRUD-API
Task is to implement simple CRUD API using in-memory database underneath.

# Assignment: CRUD API

## Description

Your task is to implement simple CRUD API using in-memory database underneath.

## Technical requirements

- Task can be implemented on Javascript or Typescript
- Only `nodemon`, `dotenv`, `cross-env`, `typescript`, `ts-node`, `eslint` and its plugins, `webpack-cli`, `webpack` and its plugins, `prettier`, `uuid`, `@types/*` as well as libraries used for testing are allowed
- Use 18 LTS version of Node.js
- Prefer asynchronous API whenever possible

## Implementation details

1. Implemented endpoint `api/users`:
- **GET** `api/users` is used to get all persons
- Server should answer with `status code` **200** and all users records
- **GET** `api/users/${userId}`
- Server should answer with `status code` **200** and and record with `id === userId` if it exists
- Server should answer with `status code` **400** and corresponding message if `userId` is invalid (not `uuid`)
- Server should answer with `status code` **404** and corresponding message if record with `id === userId` doesn't exist
- **POST** `api/users` is used to create record about new user and store it in database
- Server should answer with `status code` **201** and newly created record
- Server should answer with `status code` **400** and corresponding message if request `body` does not contain **required** fields
- **PUT** `api/users/{userId}` is used to update existing user
- Server should answer with` status code` **200** and updated record
- Server should answer with` status code` **400** and corresponding message if `userId` is invalid (not `uuid`)
- Server should answer with` status code` **404** and corresponding message if record with `id === userId` doesn't exist
- **DELETE** `api/users/${userId}` is used to delete existing user from database
- Server should answer with `status code` **204** if the record is found and deleted
- Server should answer with `status code` **400** and corresponding message if `userId` is invalid (not `uuid`)
- Server should answer with `status code` **404** and corresponding message if record with `id === userId` doesn't exist
2. Users are stored as `objects` that have following properties:
- `id` — unique identifier (`string`, `uuid`) generated on server side
- `username` — user's name (`string`, **required**)
- `age` — user's age (`number`, **required**)
- `hobbies` — user's hobbies (`array` of `strings` or empty `array`, **required**)
3. Requests to non-existing endpoints (e.g. `some-non/existing/resource`) should be handled (server should answer with `status code` **404** and corresponding human-friendly message)
4. Errors on the server side that occur during the processing of a request should be handled and processed correctly (server should answer with `status code` **500** and corresponding human-friendly message)
5. Value of `port` on which application is running should be stored in `.env` file
6. There should be 2 modes of running application (**development** and **production**):
- The application is run in development mode using `nodemon` (there is a `npm` script `start:dev`)
- The application is run in production mode (there is a `npm` script `start:prod` that starts the build process and then runs the bundled file)
7. There could be some tests for API (not less than **3** scenarios). Example of test scenario:
1. Get all records with a `GET` `api/users` request (an empty array is expected)
2. A new object is created by a `POST` `api/users` request (a response containing newly created record is expected)
3. With a `GET` `api/user/{userId}` request, we try to get the created record by its `id` (the created record is expected)
4. We try to update the created record with a `PUT` `api/users/{userId}`request (a response is expected containing an updated object with the same `id`)
5. With a `DELETE` `api/users/{userId}` request, we delete the created object by `id` (confirmation of successful deletion is expected)
6. With a `GET` `api/users/{userId}` request, we are trying to get a deleted object by `id` (expected answer is that there is no such object)
8. There could be implemented horizontal scaling for application (there is a `npm` script `start:multi` that starts multiple instances of your application using the Node.js `Cluster` API (equal to the number of logical processor cores on the host machine, each listening on port PORT + n) with a **load balancer** that distributes requests across them (using Round-robin algorithm). For example: host machine has 4 cores, `PORT` is 4000. On run `npm run start:multi` it works following way
- On `localhost:4000/api` load balancer is listening for requests
- On `localhost:4001/api`, `localhost:4002/api`, `localhost:4003/api`, `localhost:4004/api` workers are listening for requests from load balancer
- When user sends request to `localhost:4000/api`, load balancer sends this request to `localhost:4001/api`, next user request is sent to `localhost:4002/api` and so on.
- After sending request to `localhost:4004/api` load balancer starts from the first worker again (sends request to `localhost:4001/api`)
- State of db should be consistent between different workers, for example:
1. First `POST` request addressed to `localhost:4001/api` creates user
2. Second `GET` request addressed to `localhost:4002/api` should return created user
3. Third `DELETE` request addressed to `localhost:4003/api` deletes created user
4. Fourth `GET` request addressed to `localhost:4004/api` should return **404** status code for created user

5 changes: 5 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
6 changes: 6 additions & 0 deletions nodemon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"ignore": ["node_modules"],
"watch": ["./src"],
"exec": "ts-node",
"ext": "ts"
}
45 changes: 45 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "nodejs_task-3_simple-crud-api",
"version": "1.0.0",
"description": "Task is to implement simple CRUD API using in-memory database underneath.",
"main": "src/index.ts",
"scripts": {
"start:prod": "webpack && node ./dist/bundle.js",
"start:multi": "nodemon ./src/index.ts --cluster=enable",
"start:dev": "nodemon ./src/index.ts",
"test": "jest --watch --runInBand --detectOpenHandles",
"build": "webpack"
},
"repository": {
"type": "git",
"url": "git+https://github.com/webjsmaster/NodeJS_Task-3_Simple-CRUD-API.git"
},
"keywords": [],
"author": "web-js-master",
"license": "ISC",
"bugs": {
"url": "https://github.com/webjsmaster/NodeJS_Task-3_Simple-CRUD-API/issues"
},
"homepage": "https://github.com/webjsmaster/NodeJS_Task-3_Simple-CRUD-API#readme",
"devDependencies": {
"@types/jest": "^29.2.4",
"@types/node": "^18.11.17",
"@types/supertest": "^2.0.12",
"jest": "^29.3.1",
"nodemon": "^2.0.20",
"supertest": "^6.3.3",
"ts-jest": "^29.0.3",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1"
},
"dependencies": {
"@types/dotenv": "^8.2.0",
"@types/uuid": "^9.0.0",
"cross-env": "^7.0.3",
"dotenv": "^16.0.3",
"ts-node": "^10.9.1",
"uuid": "^9.0.0"
}
}
122 changes: 122 additions & 0 deletions src/__tests__/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import supertest from 'supertest';
import { server } from '..';

const createUser = {
username: 'test',
age: 27,
hobbies: ['test'],
};

const createUserErrorField = {
username: 'test',
hobbies: ['test'],
};

const updateUser = {
username: 'test2',
age: 28,
hobbies: ['test', 'test2'],
};

let userId = '';

describe('Scenario 1 - all operations', () => {
it('should be return 200 and empty array', async () => {
await supertest(server).get('/api/users').expect(200, []);
});
it('should be return 201 and create user', async () => {
const res = await supertest(server).post('/api/users').send(JSON.stringify(createUser));

userId = res.body.id;

expect(res.statusCode).toBe(201);
expect(res.body).toEqual({ id: res.body.id, ...createUser });
});
it('should be return 200 and get one user', async () => {
await supertest(server)
.get(`/api/users/${userId}`)
.expect(200, { id: userId, ...createUser });
});
it('should be return 200 and update user', async () => {
await supertest(server)
.put(`/api/users/${userId}`)
.send(JSON.stringify(updateUser))
.expect(200, { id: userId, ...updateUser });
});
it('should be return 204 and delete user', async () => {
await supertest(server).delete(`/api/users/${userId}`).expect(204);
});
it('should be return 404 when re-searching user', async () => {
await supertest(server).get(`/api/users/${userId}`).expect(404);
});
});

describe('Scenario 2 - operations for which the id is incorrectly specified', () => {
const invalidId = '24135fd3-ac82-4ef4-a0d5-b2b057524cc5';
it(`should be return 404 and message 'User not found' when trying to query for one user`, async () => {
const res = await supertest(server).post('/api/users').send(JSON.stringify(createUser));
userId = res.body.id;

await supertest(server).get(`/api/users/${invalidId}`).expect(404, { message: 'User not found' });
});
it(`should be return 404 and message 'User not found' when trying to change user`, async () => {
await supertest(server)
.put(`/api/users/${invalidId}`)
.send(JSON.stringify(updateUser))
.expect(404, { message: 'User not found' });
});
it(`should be return 404 and message 'User not found' when trying to delete user`, async () => {
await supertest(server).delete(`/api/users/${invalidId}`).expect(404, { message: 'User not found' });
});
});

describe('Scenario 3 - operations for which id is not valid', () => {
const idIsNotValid = '24135fd3-ac82-4ef4-!!d5-b2b057524cc5';
it(`should be return 400 and message 'Id not uuid' when trying to query for one user`, async () => {
const res = await supertest(server).post('/api/users').send(JSON.stringify(createUser));
userId = res.body.id;

await supertest(server).get(`/api/users/${idIsNotValid}`).expect(400, { message: 'Id not uuid' });
});

it(`should be return 404 and message 'Id not uuid' when trying to change user`, async () => {
await supertest(server)
.put(`/api/users/${idIsNotValid}`)
.send(JSON.stringify(updateUser))
.expect(400, { message: 'Id not uuid' });
});

it(`should be return 404 and message 'Id not uuid' when trying to delete user`, async () => {
await supertest(server).delete(`/api/users/${idIsNotValid}`).expect(400, { message: 'Id not uuid' });
});
});

describe('Scenario 4 - operations with other incorrect requests', () => {
it(`should be return 404 and message 'Page not found'
when endpoint specified incorrectly in GET request`, async () => {
await supertest(server).get(`/api/error`).expect(404, { message: 'Page not found' });
});

it(`should be return 404 and message 'Page not found'
when endpoint specified incorrectly in POST request`, async () => {
await supertest(server).post(`/api/error`).expect(404, { message: 'Page not found' });
});

it(`should be return 404 and message 'Page not found'
when endpoint specified incorrectly in PUT request`, async () => {
await supertest(server).put(`/api/error`).expect(404, { message: 'Page not found' });
});

it(`should be return 404 and message 'Page not found'
when endpoint specified incorrectly in DELETE request`, async () => {
await supertest(server).delete(`/api/error`).expect(404, { message: 'Page not found' });
});

it(`should be return 400 and message 'Body does not contain required fields'
when Body does not contain required fields`, async () => {
await supertest(server)
.post(`/api/users`)
.send(JSON.stringify(createUserErrorField))
.expect(400, { message: 'Invalid request body' });
});
});
23 changes: 23 additions & 0 deletions src/controllers/createUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { IncomingMessage, ServerResponse } from 'http';
import { users } from '..';
import { User } from '../types/types';
import { parseData, processingResponse, setActionMassage, validation } from '../utils';

export async function createUser(req: IncomingMessage, res: ServerResponse) {
try {
const data = await parseData(req);
const userData = JSON.parse(data as string);
const validate = validation(userData);

if (validate) {
const user: User = users.insertUser(JSON.parse(data as string));
const allUsers = users.getAll();
setActionMassage(allUsers);
processingResponse(res, 201, user);
} else {
processingResponse(res, 400, { message: 'Invalid request body' });
}
} catch (error) {
processingResponse(res, 500, { message: 'Error while passing request parameters' });
}
}
Loading