├── .gitignore ├── README.md ├── db ├── database.sql └── procedures.sql ├── docker-compose.yml ├── http └── employees.routes.http ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── app.js ├── config.js ├── controllers │ ├── employees.controller.js │ └── index.rotes.js ├── db.js ├── index.js └── routes │ ├── employees.routes.js │ └── index.routes.js └── tests ├── employees.test.js └── index.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nodejs MYSQL REST API 2 | 3 | ### Installation 4 | 5 | ``` 6 | git clone https://github.com/fazt/nodejs-mysql-restapi 7 | cd nodejs-mysql-restapi 8 | docker-compose up 9 | npm install 10 | npm run dev 11 | ``` 12 | 13 | ### TODO 14 | 15 | - [ ] upload images 16 | - [ ] create authentication and authorization 17 | - [ ] add validation 18 | - [ ] improve error handling 19 | - [ ] complete the tests 20 | - [ ] docker for production -------------------------------------------------------------------------------- /db/database.sql: -------------------------------------------------------------------------------- 1 | -- CREATE DATABASE IF NOT EXISTS companydb; 2 | 3 | -- USE companydb; 4 | 5 | CREATE TABLE employee ( 6 | id INT(11) NOT NULL AUTO_INCREMENT, 7 | name VARCHAR(45) DEFAULT NULL, 8 | salary INT(11) DEFAULT NULL, 9 | PRIMARY KEY(id) 10 | ); 11 | 12 | DESCRIBE employee; 13 | 14 | INSERT INTO employee values 15 | (1, 'Ryan Ray', 20000), 16 | (2, 'Joe McMillan', 40000), 17 | (3, 'John Carter', 50000); 18 | 19 | SELECT * FROM employee; 20 | -------------------------------------------------------------------------------- /db/procedures.sql: -------------------------------------------------------------------------------- 1 | USE company; 2 | 3 | DELIMITER $$ 4 | USE `company`$$ 5 | 6 | CREATE PROCEDURE `employeeAddOrEdit` ( 7 | IN _id INT, 8 | IN _name VARCHAR(45), 9 | IN _salary INT 10 | ) 11 | BEGIN 12 | IF _id = 0 THEN 13 | INSERT INTO employee (name, salary) 14 | VALUES (_name, _salary); 15 | 16 | SET _id = LAST_INSERT_ID(); 17 | ELSE 18 | UPDATE employee 19 | SET 20 | name = _name, 21 | salary = _salary 22 | WHERE id = _id; 23 | END IF; 24 | 25 | SELECT _id AS 'id'; 26 | END 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | db: 5 | image: mysql 6 | container_name: companydb 7 | restart: always 8 | environment: 9 | MYSQL_ROOT_PASSWORD: mypassword 10 | MYSQL_DATABASE: companydb 11 | MYSQL_USER: fazt 12 | MYSQL_PASSWORD: faztpassword 13 | ports: 14 | - 3306:3306 15 | volumes: 16 | - ./db/:/docker-entrypoint-initdb.d/ 17 | -------------------------------------------------------------------------------- /http/employees.routes.http: -------------------------------------------------------------------------------- 1 | ### get employees 2 | GET http://localhost:3000/api/employees 3 | 4 | ### 5 | POST http://localhost:3000/api/employees 6 | Content-Type: application/json 7 | 8 | { 9 | "name": "John Doe", 10 | "salary": 1000 11 | } 12 | 13 | ### Get employee by id 14 | GET http://localhost:3000/api/employees/2 15 | 16 | ### Update an employee 17 | PATCH http://localhost:3000/api/employees/2 18 | Content-Type: application/json 19 | 20 | { 21 | "name": "Ryan Ray", 22 | "salary": 500 23 | } 24 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('jest').Config} */ 2 | export default { 3 | verbose: true, 4 | testEnvironment: "node", 5 | coveragePathIgnorePatterns: ["/node_modules/"], 6 | transform: {}, 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysql-nodejs-rest-api", 3 | "version": "1.0.1", 4 | "description": "A simple REST API with Node.js, Express and MySQL", 5 | "main": "src/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "nodemon src/index.js", 9 | "start": "node src/index.js", 10 | "test": "NODE_OPTIONS=--experimental-vm-modules jest --coverage", 11 | "pretest": "cross-env NODE_ENV=test jest --clearCache" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "dotenv": "^16.0.2", 18 | "express": "^4.18.1", 19 | "mysql2": "^2.3.3" 20 | }, 21 | "devDependencies": { 22 | "cross-env": "^7.0.3", 23 | "jest": "^29.0.3", 24 | "morgan": "^1.10.0", 25 | "nodemon": "^2.0.20", 26 | "supertest": "^6.2.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import morgan from "morgan"; 3 | 4 | import employeesRoutes from "./routes/employees.routes.js"; 5 | import indexRoutes from "./routes/index.routes.js"; 6 | 7 | const app = express(); 8 | 9 | // Middlewares 10 | app.use(morgan("dev")); 11 | app.use(express.json()); 12 | 13 | // Routes 14 | app.use("/", indexRoutes); 15 | app.use("/api", employeesRoutes); 16 | 17 | app.use((req, res, next) => { 18 | res.status(404).json({ message: "Not found" }); 19 | }); 20 | 21 | export default app; 22 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | config(); 3 | 4 | export const PORT = process.env.PORT || 3000; 5 | export const DB_HOST = process.env.DB_HOST || "localhost"; 6 | export const DB_USER = process.env.DB_USER || "fazt"; 7 | export const DB_PASSWORD = process.env.DB_PASSWORD || "faztpassword"; 8 | export const DB_DATABASE = process.env.DB_DATABASE || "companydb"; 9 | export const DB_PORT = process.env.DB_PORT || 3306; 10 | -------------------------------------------------------------------------------- /src/controllers/employees.controller.js: -------------------------------------------------------------------------------- 1 | import { pool } from "../db.js"; 2 | 3 | export const getEmployees = async (req, res) => { 4 | try { 5 | const [rows] = await pool.query("SELECT * FROM employee"); 6 | res.json(rows); 7 | } catch (error) { 8 | return res.status(500).json({ message: "Something goes wrong" }); 9 | } 10 | }; 11 | 12 | export const getEmployee = async (req, res) => { 13 | try { 14 | const { id } = req.params; 15 | const [rows] = await pool.query("SELECT * FROM employee WHERE id = ?", [ 16 | id, 17 | ]); 18 | 19 | if (rows.length <= 0) { 20 | return res.status(404).json({ message: "Employee not found" }); 21 | } 22 | 23 | res.json(rows[0]); 24 | } catch (error) { 25 | return res.status(500).json({ message: "Something goes wrong" }); 26 | } 27 | }; 28 | 29 | export const deleteEmployee = async (req, res) => { 30 | try { 31 | const { id } = req.params; 32 | const [rows] = await pool.query("DELETE FROM employee WHERE id = ?", [id]); 33 | 34 | if (rows.affectedRows <= 0) { 35 | return res.status(404).json({ message: "Employee not found" }); 36 | } 37 | 38 | res.sendStatus(204); 39 | } catch (error) { 40 | return res.status(500).json({ message: "Something goes wrong" }); 41 | } 42 | }; 43 | 44 | export const createEmployee = async (req, res) => { 45 | try { 46 | const { name, salary } = req.body; 47 | const [rows] = await pool.query( 48 | "INSERT INTO employee (name, salary) VALUES (?, ?)", 49 | [name, salary] 50 | ); 51 | res.status(201).json({ id: rows.insertId, name, salary }); 52 | } catch (error) { 53 | return res.status(500).json({ message: "Something goes wrong" }); 54 | } 55 | }; 56 | 57 | export const updateEmployee = async (req, res) => { 58 | try { 59 | const { id } = req.params; 60 | const { name, salary } = req.body; 61 | 62 | const [result] = await pool.query( 63 | "UPDATE employee SET name = IFNULL(?, name), salary = IFNULL(?, salary) WHERE id = ?", 64 | [name, salary, id] 65 | ); 66 | 67 | if (result.affectedRows === 0) 68 | return res.status(404).json({ message: "Employee not found" }); 69 | 70 | const [rows] = await pool.query("SELECT * FROM employee WHERE id = ?", [ 71 | id, 72 | ]); 73 | 74 | res.json(rows[0]); 75 | } catch (error) { 76 | return res.status(500).json({ message: "Something goes wrong" }); 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /src/controllers/index.rotes.js: -------------------------------------------------------------------------------- 1 | import { pool } from "../db.js"; 2 | 3 | export const index = (req, res) => res.json({ message: "welcome to my api" }); 4 | 5 | export const ping = async (req, res) => { 6 | const [result] = await pool.query('SELECT "pong" as result'); 7 | res.json(result[0]); 8 | }; 9 | -------------------------------------------------------------------------------- /src/db.js: -------------------------------------------------------------------------------- 1 | import { createPool } from "mysql2/promise"; 2 | import { 3 | DB_DATABASE, 4 | DB_HOST, 5 | DB_PASSWORD, 6 | DB_PORT, 7 | DB_USER, 8 | } from "./config.js"; 9 | 10 | export const pool = createPool({ 11 | host: DB_HOST, 12 | user: DB_USER, 13 | password: DB_PASSWORD, 14 | port: DB_PORT, 15 | database: DB_DATABASE, 16 | }); 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import app from "./app.js"; 2 | import { PORT } from "./config.js"; 3 | 4 | app.listen(PORT); 5 | console.log(`Server on port http://localhost:${PORT}`); 6 | -------------------------------------------------------------------------------- /src/routes/employees.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { 3 | createEmployee, 4 | deleteEmployee, 5 | getEmployee, 6 | getEmployees, 7 | updateEmployee, 8 | } from "../controllers/employees.controller.js"; 9 | 10 | const router = Router(); 11 | 12 | // GET all Employees 13 | router.get("/employees", getEmployees); 14 | 15 | // GET An Employee 16 | router.get("/employees/:id", getEmployee); 17 | 18 | // DELETE An Employee 19 | router.delete("/employees/:id", deleteEmployee); 20 | 21 | // INSERT An Employee 22 | router.post("/employees", createEmployee); 23 | 24 | router.patch("/employees/:id", updateEmployee); 25 | 26 | export default router; 27 | -------------------------------------------------------------------------------- /src/routes/index.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { index, ping } from "../controllers/index.rotes.js"; 3 | 4 | const router = Router(); 5 | 6 | router.get("/", index); 7 | 8 | router.get("/ping", ping); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /tests/employees.test.js: -------------------------------------------------------------------------------- 1 | import request from "supertest"; 2 | import app from "../src/app"; 3 | import { pool } from "../src/db"; 4 | 5 | describe("Employees Routes", () => { 6 | it("should respond a list of employees", async () => { 7 | const res = await request(app).get("/api/employees"); 8 | expect(res.statusCode).toEqual(200); 9 | expect(res.body).toEqual( 10 | expect.arrayContaining([ 11 | expect.objectContaining({ 12 | id: expect.any(Number), 13 | name: expect.any(String), 14 | salary: expect.any(Number), 15 | }), 16 | ]) 17 | ); 18 | }); 19 | 20 | it("should create a new employee", async () => { 21 | const res = await request(app).post("/api/employees").send({ 22 | name: "John Doe", 23 | salary: 1000, 24 | }); 25 | expect(res.statusCode).toEqual(201); 26 | expect(res.body).toEqual( 27 | expect.objectContaining({ 28 | id: expect.any(Number), 29 | name: "John Doe", 30 | salary: 1000, 31 | }) 32 | ); 33 | }); 34 | 35 | it("should get an employee by id", async () => { 36 | const res = await request(app).get("/api/employees/1"); 37 | expect(res.statusCode).toEqual(200); 38 | expect(res.body).toEqual( 39 | expect.objectContaining({ 40 | id: 1, 41 | name: expect.any(String), 42 | salary: expect.any(Number), 43 | }) 44 | ); 45 | }); 46 | 47 | it("should delete an employee by id", async () => { 48 | const res = await request(app).delete("/api/employees/1"); 49 | expect(res.statusCode).toEqual(204); 50 | }); 51 | 52 | afterAll(async () => { 53 | await pool.end(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | import app from "../src/app"; 2 | import request from "supertest"; 3 | 4 | describe("Index Routes", () => { 5 | it("should respond welcome", async () => { 6 | const res = await request(app).get("/"); 7 | expect(res.statusCode).toEqual(200); 8 | expect(res.body).toEqual({ message: "welcome to my api" }); 9 | }); 10 | 11 | it("should respond pong", async () => { 12 | const res = await request(app).get("/ping"); 13 | expect(res.statusCode).toEqual(200); 14 | expect(res.body).toEqual({ result: "pong" }); 15 | }); 16 | }); 17 | --------------------------------------------------------------------------------