Workshop on REST API best practices
- REST API & Documentation
- Three-tiered Architecture
- Postman
- SwaggerUI
Run yarn install
and yarn run dev
on your machine after cloning.
|____refactored-doodle
| |____public
| |____index.html
| |____css
| |____styles.css
| |____js
| |____app.js
| |____src
| |____controllers
| |____app.js
| |____database
| |____db.json
| |____Workout.js
| |____services
| |____workoutService.js
| |____v1
| |____routes
| |____workoutRoutes.js
| |____index.js
- Setup and config express/scripts
// In src/index.js
const express = require("express");
const app = express();
const PORT = process.env.PORT || 3000;
// For testing purposes
app.get("/", (req, res) => {
res.send("<h2>It's Working!</h2>");
});
app.listen(PORT, () => {
console.log(`API is listening on port ${PORT}`);
});
In the package.json file:
"scripts": {
"dev": "nodemon src/index.js"
}
-
Version the API Within the structure of the API, ensure you have all of the initial functionality in a
v1
subfolder (the subsequentv2
, and so on).- Create an
index.js
file - Create a simple router similar to the one previously mentioned in the previous step
- Hook up router for v1 inside root entry point inside
src/index.js
:
/ In src/index.js const express = require("express"); const v1Router = require("./v1/routes"); const app = express(); const PORT = process.env.PORT || 3000; app.use("/api/v1", v1Router); app.listen(PORT, () => { console.log(`API is listening on port ${PORT}`); });
- Create an
-
Name resources in plural Where you may have multiple controllers/services/routes/etc., it's best for clarity's sake to keep the folders named in plural.
-
Define endpoints and test them:
// In src/v1/routes/workoutRoutes.js
const express = require("express");
const router = express.Router();
router.get("/", (req, res) => {
res.send("Get all workouts");
});
router.get("/:workoutId", (req, res) => {
res.send("Get an existing workout");
});
router.post("/", (req, res) => {
res.send("Create a new workout");
});
router.patch("/:workoutId", (req, res) => {
res.send("Update an existing workout");
});
router.delete("/:workoutId", (req, res) => {
res.send("Delete an existing workout");
});
module.exports = router;
- Hook v1 router into entry point (
src/index.js
):
// In src/index.js
const express = require("express");
const v1WorkoutRouter = require("./v1/routes/workoutRoutes");
const app = express();
const PORT = process.env.PORT || 3000;
app.use("/api/v1/workouts", v1WorkoutRouter);
app.listen(PORT, () => {
console.log(`API is listening on port ${PORT}`);
});
- Create a controller method for each endpoint and refactor router:
// In src/controllers/workoutController.js
const getAllWorkouts = (req, res) => {
res.send("Get all workouts");
};
const getOneWorkout = (req, res) => {
res.send("Get an existing workout");
};
const createNewWorkout = (req, res) => {
res.send("Create a new workout");
};
const updateOneWorkout = (req, res) => {
res.send("Update an existing workout");
};
const deleteOneWorkout = (req, res) => {
res.send("Delete an existing workout");
};
module.exports = {
getAllWorkouts,
getOneWorkout,
createNewWorkout,
updateOneWorkout,
deleteOneWorkout,
};
//in v1/routes/workoutRoutes.js
router.get("/", workoutController.getAllWorkouts);
router.get("/:workoutId", workoutController.getOneWorkout);
router.post("/", workoutController.createNewWorkout);
router.patch("/:workoutId", workoutController.updateOneWorkout);
router.delete("/:workoutId", workoutController.deleteOneWorkout);
-
Create service layer:
const getAllWorkouts = () => { return; }; const getOneWorkout = () => { return; }; const createNewWorkout = () => { return; }; const updateOneWorkout = () => { return; }; const deleteOneWorkout = () => { return; }; module.exports = { getAllWorkouts, getOneWorkout, createNewWorkout, updateOneWorkout, deleteOneWorkout, };
"It's also a good practice to name the service methods the same as the controller methods so that you have a connection between those. Let's start off with just returning nothing."
-
Use methods inside of workout controller so it can communicate with our service layer:
const workoutService = require("../services/workoutService");
const getAllWorkouts = (req, res) => {
const allWorkouts = workoutService.getAllWorkouts();
res.send("Get all workouts");
};
const getOneWorkout = (req, res) => {
const workout = workoutService.getOneWorkout();
res.send("Get an existing workout");
};
const createNewWorkout = (req, res) => {
const createdWorkout = workoutService.createNewWorkout();
res.send("Create a new workout");
};
const updateOneWorkout = (req, res) => {
const updatedWorkout = workoutService.updateOneWorkout();
res.send("Update an existing workout");
};
const deleteOneWorkout = (req, res) => {
const updatedWorkout = workoutService.updateOneWorkout();
res.send("Delete an existing workout");
};
module.exports = {
getAllWorkouts,
getOneWorkout,
createNewWorkout,
updateOneWorkout,
deleteOneWorkout,
};
-
Create DB/DB files. In this project,
db.json
is created for the data andWorkout.js
for the workout-specific methods. -
Create a data-access layer and return all workouts from the
db.json
file. -
To be able to parse the request body sent back, install the
body-parser
package and configure it in thesrc/index.js
file. This will help receive the JSON data inside the controller underreq.body
. -
Test in Postman by creating a POST request to
localhost:3000/api/v1/workouts
and a request body in JSON format. The missing properties ('id', 'createdAt', 'updatedAt`) will be added by the API before inserting it through the workout service. -
Inside the workout controller method
createNewWorkout
, extract the body from the request object, do some validation, and then pass it in as an argument to the workout service. For improving this, a third party package can be used. -
Go into the workout service and receive the data inside the new createWorkout method. Add the missing properties to the object and pass it as a new method inside the DB.
-
Create a Util function to overwrite the JSON file and persist the data. In the file, require fs:
const fs = require("fs");
const saveToDatabase = (DB) => {
fs.writeFileSync("./src/database/db.json", JSON.stringify(DB, null, 2), {
encoding: "utf-8",
});
};
module.exports = { saveToDatabase };
- Use the Util function in the
Workout.js
file:
const createNewWorkout = (newWorkout) => {
const isAlreadyAdded =
DB.workouts.findIndex((workout) => workout.name === newWorkout.name) > -1;
if (isAlreadyAdded) {
return;
}
DB.workouts.push(newWorkout);
saveToDatabase(DB);
return newWorkout;
};
//also add that function into the module.exports