/Api-Restful-ts

How to create a API RESTful

Primary LanguageTypeScriptMIT LicenseMIT

Api Restful with TypeScript


Install packages and dependencies


Start the project with command line.

* `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 named app.ts
  • To hot reload of the application, inside of package.json we gonna write inside of script 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: folder

(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

Integrating express


  • 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 named default.ts to store sensitive variables.
    • Inside the file we export the variables, creating one object like that:
    • config
    export default {
        port: 3000,
    }
  • 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}`)
})

API test routes


  • 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. router

  • 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!")
})
  • Your file should be like this: router2

  • 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: mainFile

Creating database with MongodoDB Atlas


  • 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!
  1. Create a new project tuto

  2. Choice a name and click in next tuto

  3. Click in create project tuto

  4. Then in DATABASES, click in Build Database tuto

  5. Choice the plain free tuto

  6. Just click in Create Cluster tuto

  7. Go to Network Acess tuto

  8. Click in Add IP Address, then click in Add Current IP Adress and confirm tuto

  9. Go to Database Acess and click in Add New Database User tuto

  10. 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 tuto

  11. In the end just gonna click in Add User tuto

I strongly recommend you reed the documentation to know what are you doing!


connecting to database


First let's to mongoDB website and copy the URL from our database!

  1. Go to databases and click on Connect db
  2. Then click in Connect your application db
  3. Finally copy the URL from your database db
  • 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"
    • db
      • Then we gonna change were is <passworld> to yours password that we generate!
  • Creating connection to database
    • Now let's create one file named db.ts in config folder.
    • Import two modules we installed on the beginning of the project: mongoose and config
      •   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
    • db
  • Importing to main file!
    • Inside of app.ts we gonna make two modifications.
    • First we gonna import the file db.ts.
      • import db from "../config/db"
    • Second on the listen we gonna call db as await, in this way the application just gone run if was connect to the database!
    • db

Creating environment variables


  • We gonna create one file to keep yours sensitives varibles, like API key, Database password etc...
    • First on root, create one file named .env. dotenv
    • 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
    • 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: dotenv
    • Remember to change this variables to yours, this variables are from my fictional database from this repository!
  • 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 to src/app.ts and in the first line call the module.
      • require("dotenv").config() dotenv
    • To call the variables from file .env in config/default.ts we use the function process.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`
    }
    dotenv

Integration of Wiston


  • 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 folder config

    • Now let's import theses modules: config and winston looger
    • You can read more about Winston here
  • Now we go to config/default.ts and we gonna create one other attribute on ours object

    • The new attribute is named env and receive on string on value development.
      • env: "development"
      • logger
  • 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
    }
  • 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 ours env from config.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 or warn to us!
  • 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)
  • 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",})
    ]
  • 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!
      1. level
      2. levels
      3. format
      4. transports
    const Logger = winston.createLogger({
        level: level(),
        levels,
        format,
        transports
    })
    • Now we export the Logger
  • To finish we can import this in any file we want.

    • To call Logger, we replace the console.log() to Logger.info() / Logger.error() / Logger.warn() and etc... logger logger

Configurating Morgan


  • 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
    • Go to our main file app.ts and import morgan.
      • import morganMiddleware from "./middleware/morganMiddleware"
    • Now let's make this middleware be in level of application!
      • app.use(morganMiddleware)
    • To test i going use Thunder Client to make HTTP request to our API, thunder-client

Creating Model


  • Working with MVC (Model View Controller)
    • To start lets configure our models
    • Let's create one folder on src named models
    • 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 and Schema from mongoose if you are using mongoDB
      • import {model, Schema} from "mongoose"
  • 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)

Creating Controller


  • Start with controllers
    • In src we going to create a folder named controllers and inside of folder, let's create one file named movieControllers.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"
  • 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!

Insert datas


  • 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) {
              
          }
      }
  • Catch
    • Inside of catch we going pass a error log from Logger and type the error as any
    • 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}`)
        }
    }
    • Now you can test using POSTMAN, ThunderClient or any other tool! In my case i'll use Thunder Client! insert insert (Okay, i wrote wrong Fellowship... you can judge me!)

Express-validation


  • 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 named handleValidation.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
  • 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 variable erros 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
              })
          }
  • Import middleware
    • Let's go to src/router.ts and import our middleware
      • import { 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)

Validating movies


  • 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 name movieCreateValidation.
      •   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.
  • 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)
    error
    • 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
  • 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 method CustomValidator.
      • 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)
        ]
    }
  • 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!")
        ]
    }

Get movie by id


  • 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}`)
        }
    }
  • 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!

Get all movies


  • 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.
  • 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 named movie but is a POST router and this is a GET.
      • .get("/movie", findAllMovies)

Remove movie by Id


  • 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 from findById()
    • 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 function deleteMovie.

      • import { createMovie, deleteMovie, findAllMovies, findMovieById } from "./controllers/movieControllers"
    • Now let's create the router

      • .delete("/movie/:id", deleteMovie)
    • And it's ready!


Update datas


  • 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}`)
            }
        }
  • 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)

Ending


  • 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!