Install packages and dependencies
* `npm init -y` & ` tsc --init`
- Now, we gonna install the dependencies for development and separate they from dependencies like express
npm install config dotenv express express-validator mongoose morgan winston
- (These dependencies are not for development state!)npm install @types/config @types/express @types/mongoose @types/morgan @types/node ts-node-dev typescript --save-dev
- (They are!)
- With all dependencies installed, we create one folder named
src
and inside, create one file namedapp.ts
- To hot reload of the application, inside of
package.json
we gonna write inside ofscript
this code ->"dev": "ts-node-dev --respawn --transpile-only src/app.ts"
- To test, we gonna write a simple line on app.ts, this line is just a classic "HELLO WORLD".
console.log("Hello World From Node")
- Your project folder should be like this:
(Observation: .gitignore, .gitattribute, README.md and img-reposi are for this repository where you are reading, so you can ignore in your project!)
- Let's run our project in command line, call the script dev:
npm run dev
- Let's start express on
app.ts
importing the modules, create middleware to accept JSON data and start the server.- We are using the module config, then we gonna create one folder named
config
and create a file nameddefault.ts
to store sensitive variables. - Inside the file we export the variables, creating one object like that:
export default { port: 3000, }
- We are using the module config, then we gonna create one folder named
- Now we import to ours main file.
import express from "express"
import config from "config"
const app = express()
//JSON middleware
app.use(express.json())
// Config PORT
const port = config.get<number>("port")
app.listen(port,async()=>{
console.log(`Server online on port: ${port}`)
})
-
To start with routes, we gonna create a new file named
router.ts
inside the file src. -
With the file created, let's import functionalities from express, start the code creating one variable to recibe the this functionalities.
-
Okay! now we wanna create the first router from this project!
- First i gonna to remember you that this is a outer filer, so we need to export to our main file!
-
The syntax is not much difference that we already seen, but we need to export each one of ours routers! like that:
export default router.get("/test", (req:Request, res:Response)=>{
res.status(200).send("API working!")
})
-
Now to finish, let's back to our main file
app.ts
and import our router!
//Router
import router from './router'
app.use("/api/", router)
- Now, all routers arriving from the
routes
receives the prefix /api/ - Your
app.ts
should be like this:
- First thing you need to do is create a account on MongoDB here
- Then you need create a database on mongoDB Atlas, i don't gonna explain all to you how to do this, but you can read how to do on offical documentation from mongoDB here
- BUT i gonna show you how to do it in a simple way!
-
Click in Add IP Address, then click in Add Current IP Adress and confirm
-
Choice one name to our user and click in Autogenerate Secure Password, copy the password and user name to one safe place... We gonna need this later
I strongly recommend you reed the documentation to know what are you doing!
First let's to mongoDB website and copy the URL from our database!
- Go to databases and click on Connect
- Then click in Connect your application
- Finally copy the URL from your database
- Creating configs to database
- Now we have the URL from ours database, so lets go to file of configurations that we created previously
config/default.ts
. - Inside of object he already created, let's create a new variable named
dbUrl
and it gonna receive our URL.dbUrl: "mongodb+srv://AshPalha:<password>@cluster0.xdzpwwf.mongodb.net/?retryWrites=true&w=majority"
- Then we gonna change were is
<passworld>
to yours password that we generate!
- Then we gonna change were is
- Now we have the URL from ours database, so lets go to file of configurations that we created previously
- Creating connection to database
- Now let's create one file named
db.ts
inconfig
folder. - Import two modules we installed on the beginning of the project:
mongoose
andconfig
-
import mongoose from "mongoose" import config from "config"
-
- Now we gonna create one async function to connect, named connect
- Then we gonna catch our URL to database using the module
config
.const url = config.get<string>("dbUrl")
- Done it we create a Try Catch to connection to tratament for errors!
-
try { } catch (err) { }
-
- Inside the Try, we gonna call from mongoose and use one method named
connect
and pass our URL as parameter.-
await mongoose.connect(url) console.log("Successful connection to database!")
-
- Inside the catch, we just gonna make a log to the error!
-
console.log("Error to connect to database") console.log(`Error: ${err}`)
-
- Now we export the function!
export default connect
- Now let's create one file named
- Importing to main file!
- We gonna create one file to keep yours sensitives varibles, like API key, Database password etc...
- First on root, create one file named
.env
. - Inside of file we keep any variable what we want.
- In your case, lets keep the username and password from database for now
- Observation: The variables always are in upcase
- Like that:
VAR_NAME=data
- Like that:
- Observation 2: If you want to post your pessoal project on GitHub, on
.gitignore
put the file.env
inside! I don't gonna do that because i want you to learn from this! - For you see better, this is the code:
- Remember to change this variables to yours, this variables are from my fictional database from this repository!
- First on root, create one file named
- Now let's go to
config/default.ts
and call ours environment variables!- But first we need to call the module
dotenv
, so let's go tosrc/app.ts
and in the first line call the module. - To call the variables from file
.env
inconfig/default.ts
we use the functionprocess.env.NAME_FROM_VARIABLE
- We gonna keep the two of yours variables on constants
- Like that:
const db_name = process.env.DB_NAME const db_pass = process.env.DB_PASS
- Then in variable
dbUrl
let's concatenate on the string
const db_name = process.env.DB_NAME const db_pass = process.env.DB_PASS export default { port: 3000, dbUrl: `mongodb+srv://${db_name}:${db_pass}@cluster0.xdzpwwf.mongodb.net/?retryWrites=true&w=majority` }
- But first we need to call the module
-
To start with Wiston, we need understand what are this!
- We gonna use Wiston to improve ours LOGS on terminal
-
So let's create a new file named
logger.ts
on folderconfig
- Now let's import theses modules:
config
andwinston
- You can read more about Winston here
- Now let's import theses modules:
-
Now we go to
config/default.ts
and we gonna create one other attribute on ours object -
Done this, we go back to file
logger
and we gonna create a series of configs that gonna describe how our application will behave.- First we create a object named
levels
that gonna describe the level of errors of ours aplication!
const levels = { error: 0, warn: 1, info: 2, http: 3, debug: 4 }
- First we create a object named
-
Now let's create one variable what's going to call one function to receive what environment we are working!
- The variable is named
level
and gonna receive oursenv
fromconfig.ts
and validate the information
const level = ()=>{ const env = config.get<string>("env") || "development" const isDevelopment = env === "development" return isDevelopment ? "debug" : "warn" }
- The function going to return or
debug
orwarn
to us!
- The variable is named
-
Now we going to create one object to receive colors for ours error
- The object is equal to
levels
but going receive string of colors
const colors = { error: "red", warn: "yellow", info: "green", http: "magenta", debug: "white" }
- Now we add this colors on Winston.
wiston.addColors(colors)
- The object is equal to
-
Now we going to format ours log messages.
- This format are from the log message that going to appers in ours terminal
- Let's put the time, color and message
const format = winston.format.combine( winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), winston.format.colorize({ all: true }), winston.format.printf( (info) => `${info.timestamp} - ${info.level}: ${info.message}` ) )
-
Now we gonna create others constants to create erros files
- We going to use one method from Winston named
transports
. - This transports are to create files from erros, we can organize ours erros by folders if we want.
const transports = [ new winston.transports.Console(), new winston.transports.File({ filename: "logs/erros/error.log", level: "error" }), new winston.transports.File({filename: "logs/all.log",}) ]
- We going to use one method from Winston named
-
Okay, we create where the logs are, now we need to instance all of this!
- We pass one object with all variables that we create!
- level
- levels
- format
- transports
const Logger = winston.createLogger({ level: level(), levels, format, transports })
- Now we export the
Logger
- We pass one object with all variables that we create!
-
To finish we can import this in any file we want.
- Start with Morgan.
- The Morgan will make our log be more complete
- First we going to create one folder to middleware on
src
- Then creating one file named
morganMiddleware.ts
- Now we going import inside the file ours modules.
import morgan, {StreamOptions} from "morgan"; import config from "config" import Logger from "../../config/logger";
- Creating config to morgan
- After we import ours modules, let's create objects to read http requests
const stream: StreamOptions = { write: (message)=>Logger.http(message) }
- After this, let's create one validation to skip if are in development environment
const skip = ()=>{ const env = config.get<string>("env") || "development" return env !== "development" }
- Now we going create one instance of class morgan and join the valiables that we create and in the end we export.
const morganMiddleware = morgan( ":method :url :status :res[content-length] - :response-time ms", {stream, skip} ) export default morganMiddleware
- Import morgan to other files
- Working with MVC (Model View Controller)
- To start lets configure our models
- Let's create one folder on
src
namedmodels
- let's insert the entities that will allude to our collections of our database
- Start Model
- In the folder what we create, lets create one file named
Movie.ts
- Whenever we create a new model on TypeScript we going import
model
andSchema
from mongoose if you are usingmongoDB
import {model, Schema} from "mongoose"
- In the folder what we create, lets create one file named
- Creating Schema
- Let's create a Schema! This schema is a object with all properties we gonna insert on database
const movieSchema = new Schema({ title: {type: String}, rating: {type: Number}, description: {type: String}, director: {type: String}, stars: {type: Array}, poster: {type: String} }, { timestamps: true })
- Now we export.
export const MovieModel = model("Movie", movieSchema)
- Start with controllers
- In
src
we going to create a folder namedcontrollers
and inside of folder, let's create one file namedmovieControllers.ts
- Inside of this file we going to work with express, so let's import it
import {Request, Response} from "express"
- After this, let's import our Model that we create in last session.
import { MovieModel } from "../models/Movie"
- Now to finish ours imports, let's import our
Logger
import {Request, Response} from "express" //Model import { MovieModel } from "../models/Movie" // Logger import Logger from "../../config/logger"
- In
- Funtions
- Now we going create ours functions to creation, reading and etc...
- Let's start with one creation function for the movie!
- All functions going to be Async because we going work with database then we need to hold they response to proceed
- Creation Function and Link to Router
- Let's create one simple function to test
export async function createMovie(req:Request, res:Response){ return res.status(200).send("Controller Working") }
- Now let's go to file
src/router.ts
- Then let's configurate your router to new router.
- In export router, let's create a new POST router and call ours function that we create
//exporting routers export default router .get("/test", (req:Request, res:Response)=>{ res.status(200).send("API is working!") }) //New .post("/movie", createMovie)
- Now the link between our controllers and routers are ready!
- Then we can configure ours controller properly!
- Let's go
controllers/movieControllers.ts
. - inside of ours function, let's delete the return and replace to one Try/Catch.
//Creation export async function createMovie(req:Request, res:Response){ try { } catch (err) { } }
- Inside of try let's create a variable to get the data from
request
.const data = req.body
- Now let's create other variable to receive our Model and insert in our database the
data
.const movie = awai MovieModel.create(data)
- Then we going to return one status code 201 and pass a JSON message of
data
, like that:return res.status(201).json(data)
export async function createMovie(req:Request, res:Response){ try { const data = req.body const movie = await MovieModel.create(data) return res.status(201).json(movie) } catch (err) { } }
- Inside of try let's create a variable to get the data from
- Catch
- Inside of
catch
we going pass a error log from Logger and type the error asany
- The final code should be like that:
export async function createMovie(req:Request, res:Response){ try { const data = req.body const movie = await MovieModel.create(data) return res.status(201).json(movie) } catch (err:any) { Logger.error(`Error: ${err.message}`) } }
- Inside of
- We going create middleware to validation now using Express-validation
- This middleware will serve as a "joker" to deal with all validations of our system, after we will create validation to each one of ours entities.
- Let's start
- In
middleware
folder, let's create a file namedhandleValidation.ts
. - After creation let's import the modules.
-
import { Request, Response, NextFunction } from "express" import { validationResult } from "express-validator"
-
- Now we'll create a middleware to take all
errors
from the next function that we will create and we'll treat they
- In
- Create Middleware
- Now let's create a arrow function in a variable named
validate
, like this:
export const validate = (req:Request, res:Response, next:NextFunction)=>{ //code... }
- Inside of the function let's create a variable what's take erros from request, we going use the module express-validator on this variable
const errors = validationResult(req)
- After this we will create a validation using
if
to see if our variableerros
are empty or not.-
if(errors.isEmpty()){ return next() }
-
- If are no erros on the application, the function
next()
is named and the application run normal! - But if are erros... We going create one array of objects to rescue all erros and return to user one bad request (422) from server and show all erros!
- For theses erros we will use the method
map()
to go through all itens and push into our object.-
export const validate = (req:Request, res:Response, next:NextFunction)=>{ const errors = validationResult(req) if(errors.isEmpty()){ return next() } //Creating array empty const extratecErros: object[] = [] //Push all erros into our array errors.array().map((err)=>{ extratecErros.push({[err.param]: err.msg}) }) //Return a bad request return res.status(422).json({ erros: extratecErros }) }
-
- Now let's create a arrow function in a variable named
- Import middleware
- Let's go to
src/router.ts
and import our middlewareimport { validate } from "./middleware/handleValidation"
- Now let's put the
validate
inside the route/movie
.
export default router .get("/test", (req:Request, res:Response)=>{ res.status(200).send("API is working!") }) .post("/movie", validate, createMovie)
- Let's go to
- In this session we will finish the validation that we start in last session
- Start validation
- Let's create another middleware
middleware/movieValidation.ts
- To start the file let's import the
body
from express-validator and create a function with namemovieCreateValidation
.-
import { body } from "express-validator" export const movieCreateValidation = ()=>{ }
-
- Inside the function let's use the module
body
to make ours validation.- You can read the documentation of express-validator here and know all the methods of validation from this library.
- Let's create another middleware
- Create first validation.
- Now we create the function, let's create a validation from the tittle of the movie.
- We will return a Array to the validation.
export const movieCreateValidation = ()=>{ return[ body("title").isString().withMessage("The title is mandatory") ] }
- This function get the title from the Request and validation if are String if not generate a error with message "The title is mandatory"
- This already are one validation and we can use import this on route that we will use, in ours case
src/router.ts
in router/movie
.-
.post("/movie", movieCreateValidation(),validate, createMovie)
-
- What's appears here is the middleware handleValidation get the errors from movieCreateValidation and show to user!
- Make better validations
- You know now how to make validations with
express-validator
but until now we just make a title validation, let's make others! - As you probleby remember our Modal for database have this camps:
-
title: {type: String}, rating: {type: Number}, description: {type: String}, director: {type: String}, stars: {type: Array}, poster: {type: String}
- So we can validate any one! let's start with
rating
.- Just like the title we start the validate with
body("campName")
body("rating").isNumeric().withMessage("The rating must be a number!")
- The method
withMessage()
show the message if prerequisites are not being met
- Just like the title we start the validate with
- You know now how to make validations with
- Custom validators
- With custom validators we can create one function inside the method
custom()
and make ours own validation. - Let's import from
express-validator
the methodCustomValidator
.import { body, CustomValidator } from "express-validator"
- Now let's create our validation by function!
const ratingNote: CustomValidator = (value:number)=>{ if(value < 0 || value > 10){ throw new Error("The rating should be between 0 to 10!") } return true }
- After we create the function, let's put on method
custom()
.
export const movieCreateValidation = ()=>{ return[ body("title").isString().withMessage("The title is mandatory"), body("rating").isNumeric().withMessage("The rating must be a number!").custom(ratingNote) ] }
- With custom validators we can create one function inside the method
- Finish validations
- To finish let's make validations to all camps from model inside the functions validation!
export const movieCreateValidation = ()=>{ return[ body("title").isString().withMessage("The title is mandatory"), body("rating").isNumeric().withMessage("The rating must be a number!").custom(ratingNote), body("director").isString().withMessage("The director name is mandatory!"), body("description").isString().withMessage("The description is mandatory"), body("poster").isURL().withMessage("The poster must be URL!") ] }
- Now we already have task ready we can make actions for reading
- Creating GETTERS to database
- Go to
controllers/movieControllers.ts
- Let's create a new function to get filme by id
- First we create a Async function and put a Try/Catch.
- The id will come from parameters of the request
export async function findMovieById(req:Request, res:Response){ try { const id = req.params.id const movie = await MovieModel.findById(id) if(!movie){ return res.status(404).json({error: "Movie not found!"}) } return res.status(200).json(movie) } catch (err: any) { Logger.error(`Error: ${err.message}`) } }
- Go to
- Creating router
- After the creation go to our router file on src and import the function.
import { createMovie, findMovieById } from "./controllers/
- Now let's create a GET router with dinamic url.
.get("/movie/:id", findMovieById)
- And is WORKING!
- After the creation go to our router file on src and import the function.
- To create one query to all datas from database with MongoDB is really simple.
- Creating function
- In the same file that we create last function we will create a async function named
findAllMovies
, then make a Try/Catch just like the last function-
export async function findAllMovies(req:Request, res:Response){ try{ const movies = await MovieModel.find() return res.status(200).json(movies) }catch(err:any){ Logger.error(`Error: ${err.message}`) } }
-
- When we use the method
find()
and we dont pass parameters, the mongoDB understand that we want all the datas.
- In the same file that we create last function we will create a async function named
- Creating router
- After the creation go to our router file on src and import the function.
import { createMovie, findAllMovies, findMovieById } from "./controllers/
- Now create a GET router named
movie
, we already have a router namedmovie
but is a POST router and this is a GET..get("/movie", findAllMovies)
- After the creation go to our router file on src and import the function.
- To remove a data from our database we will use the same structure from
findMovieById
function. - Creating function
-
export async function deleteMovie(req:Request, res:Response){ try { const id = req.params.id const movie = await MovieModel.findById(id) if(!movie){ return res.status(404).json({error: "Movie not Found!"}) }else{ await movie.delete() return res.status(200).json({message: "Movie deleted!"}) } } catch (err: any) { Logger.error(`Erro: ${err.message}`) } }
- Notice in the end of try we use the method
delete()
, this method delete the data that we get fromfindById()
- Is the same structure that we use to get the movie by id
-
- Creating Router
-
Now we will create a router using the
delete
http method. -
Go to
src/router.ts
and import the functiondeleteMovie
.import { createMovie, deleteMovie, findAllMovies, findMovieById } from "./controllers/movieControllers"
-
Now let's create the router
.delete("/movie/:id", deleteMovie)
-
And it's ready!
-
- Now let's create a function to update datas from our database.
- Creating function
- Is again the same structure from
findById
. - But now we get the data from body what's be updated and use the method
updateOne()
use the id as parameter and the data -
export async function updateMovie(req:Request, res:Response){ try { const id = req.params.id const data = req.body const movie = await MovieModel.findById(id) if(!movie){ return res.status(404).json({error: "Movie not found!"}) } await MovieModel.updateOne({_id: id}, data) return res.status(200).json({message: "Movie updated!", data}) } catch (err: any) { Logger.error(`Erro: ${err}`) } }
- Is again the same structure from
- Creating router
- Let's import the function to
src/router.ts
.import { createMovie, deleteMovie, findAllMovies, findMovieById, updateMovie } from "./controllers/movieControllers"
- Now creating the router using
patch
as http method and put your middlewares to validate..patch("/movie/:id", movieCreateValidation(), validate, updateMovie)
- Let's import the function to
- Okay guys! now we finish one entire API RESTful and i expect that you guys learn as much i did write this repository. We created routers and connect to databases, learn how to use the http methods and validations middlewares, make customs loggers to see the errors from our application!
- And probleby have some errors on my english because i challeger myself to write all this application in english to test my skills on NODE.JS and english haha but i really expect that you had fun reading this and learn a lot!
- Thanks! and see you and another repository!