DEV Community

Omar Elwakeel
Omar Elwakeel

Posted on • Edited on

Microservices, express-react app (Part 1) Let the journey begin!

Handle errors

Micro-services, I promise! but step by step, stay with me and I promise you won't regret it!!

This is the start of a series of posts discussing Micro-services, how to use Docker, Kubernetes and make your own CI/CD Workflow to deploy your app with cool automation. So the title is confusing, I know!!

Part 2

You can clone the app from here, or as I strongly recommend, go along step by step

But why not take it easy and see all the cool things we can discuss along the way, so I decided to take this big article and divide it into small articles for two reasons

First to discuss all the small code features in a more focused way, and the second reason is to give you a chance to build the app with me and go along this articles smoothly.

So this part will only discuss how to build a simple typescript express app, handle errors and use appropriate middlewares.

If you are ready, please seat belts and let's type some code!!

First make a directory with the app name, we are going to make a posts app so folder with name posts. This directory will have all our services at the end. Here's how we want to end.

Folder structure

let's setup our app structure

inside the root directory terminal (posts-app)

git init
Enter fullscreen mode Exit fullscreen mode

to initialize a git repository, after a while we will connect to our github account

inside the terminal (posts-app/posts)

npm init --y

Enter fullscreen mode Exit fullscreen mode

to initialize npm and generate our package.json file

inside posts service (posts-app/posts)

npm install express @types/express express-validator typescript ts-node-dev  mongoose 
Enter fullscreen mode Exit fullscreen mode

these are the required dependencies for now, what for ?

express and @types/express for the app server instance
express-validator for validation of request body
typescript ts-node-dev to use typescript in code and compile on development
mongoose for connection to db as we are going to use mongodb

inside the terminal (posts-app/posts)

tsc --init
Enter fullscreen mode Exit fullscreen mode

this will generate the tsconfig file which is responsible for the typescript configuration, you have to do it!

lets create some files and folders, here how we want to end

Folder structure

inside .env (posts-app/posts)

PORT=3000
MONGO_URI=mongodb://localhost:27017/posts-app
Enter fullscreen mode Exit fullscreen mode

Some variables we will need, port to listen to and a mongo url to use on db connection

inside app.ts (posts-app/posts)

import express, { Request, Response } from "express";
import { json } from "body-parser"
import { errorHandler } from "./middlewares/error-handler";
import { newPostRouter } from "./routes/new";

const app = express();

app.use(json())

app.use(newPostRouter)

app.all('*', (req:Request, res:Response) => {
    return res.status(404).send([{message:'Not found'}])
})

app.use(errorHandler)

export default app;
Enter fullscreen mode Exit fullscreen mode

*we create an app instance from express
*use json body parser for json objects in request body to be parsed
*we use a router which we will know about shortly
*app.all uses a wild card to say that if the request is not handled by any of the previous handlers then return a 404 response
*we use an error handler so any error that is thrown inside the app is handled by this handler, also we will see shortly
*export this instance to be used in index.ts, the start of our app

inside index.ts (posts-app/posts)

import express, { Request, Response } from "express";
import app from './app'
import mongoose from "mongoose";

import dotenv from 'dotenv'
dotenv.config();

const start = async () => {

    if(!process.env.MONGO_URI){
        throw new Error('MONGO_URI must be defined')
    }

    try {
        await mongoose.connect(process.env.MONGO_URI)
        console.log('Mongo db is connected');
    } catch (error) {
        console.error(error);
    }

    const port = process.env.PORT || 3000

    app.listen(port, () => {
        console.log('Posts service started....');
    });

}

start();
Enter fullscreen mode Exit fullscreen mode

*we use dotenv to config our variables in .env
*we connect to db using mongoose
*we use imported app instance to listen to the required port
*we do all that in a start function in a modest fashion way

inside post.ts (posts-app/posts/models)

import mongoose from "mongoose";

interface PostAttributes {
    title:string;
    content:string;
    owner:string;
}

interface PostDocument extends mongoose.Document{
    title:string;
    content:string;
    owner:string;   
}

interface PostModel extends mongoose.Model<PostDocument>{
    build(attributes:PostAttributes):PostDocument;
}

const postSchema = new mongoose.Schema({
    title:{
        type:String,
        required:true
    },
    content:{
        type:String,
        required:true
    },
    owner:{
        type:String,
        required:true
    },
}, {
    timestamps:true,
    toJSON:{
        transform(doc, ret){
            ret.id = ret._id
        }
    }
})


postSchema.statics.build = (attributes:PostAttributes) => {
    return new Post(attributes);
}

const Post = mongoose.model<PostDocument, PostModel>('Post', postSchema)

export default Post;
Enter fullscreen mode Exit fullscreen mode

*we define the attributes used to define a post document in db
*we define a build function to return a new post
*we adjust some configs before export the Post model

inside error-handler.ts (posts-app/posts/middlewares)

import express, { NextFunction, Request, Response } from "express";
import { CustomError } from "../errors/custom-error";

export const errorHandler = (
    error:Error,
    req:Request,
    res:Response,
    next:NextFunction
    ) => {
        if(error instanceof CustomError){
            return res.status(error.statusCode).send({errors:error.generateErrors()})
        }

        console.error(error);
        res.status(400).send({
            errors:[{message:"Something went wrong"}]
        })
    }
Enter fullscreen mode Exit fullscreen mode

*we define a function that will be responsible for handling errors, how express will know, by convention it will search for a function that has 4 input parameters, their order is very important, the error instance comes first!!
*we check if the error is one of the instances we will create soon or not and return the response

inside validate-request.ts (posts-app/posts/middlewares)

import { NextFunction, Request, Response } from "express";
import { validationResult } from "express-validator";
import { RequestValidationError } from "../errors/request-validation-error";

export const validateRequest = (
    req:Request,
    res:Response,
    next:NextFunction
    )=>{
        const errors = validationResult(req);

        if(!errors.isEmpty()){
            throw new RequestValidationError(errors.array())
        }

        next();
    }
Enter fullscreen mode Exit fullscreen mode

*we define a function that will be responsible for validating the body of the incoming request, if not valid it will throw an error that will then handled by the error handler we just created, if now it will call the next function that will pass the request to the next handler.

inside custom-error.ts (posts-app/posts/errors)

export abstract class CustomError extends Error{
    abstract statusCode:number;

    constructor(message:string){
        super(message)
        Object.setPrototypeOf(this, CustomError.prototype);
    }

    abstract generateErrors():{message:string, field?:string}[];
}
Enter fullscreen mode Exit fullscreen mode

*we define a custom error class to be extended by all the error classes we are going to use, why do so? To have a stable error response body and make it easy to catch and handle errors inside our app.

inside request-validation-error.ts (posts-app/posts/errors)

import { ValidationError } from "express-validator";
import { CustomError } from './custom-error'

export class RequestValidationError extends CustomError{
    statusCode = 400;
    constructor(public errors:ValidationError[]){
        super('Invalid request parameters');
        Object.setPrototypeOf(this, RequestValidationError.prototype);
    }

    generateErrors(){
        return this.errors.map(error => {
            return { message:error.msg, field:error.value }
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

*we define an error class taking control of the validation of request body, extending the custom error class

inside new.ts (posts-app/posts/routes)

import express, { Request, Response } from "express";
import { body } from "express-validator";
import { validateRequest } from "../middlewares/validate-request";
import Post from "../models/post";

const router = express.Router();

router.post(
    '/api/posts', 
    [
        body('title').not().isEmpty().withMessage('Title must be provided').isString().withMessage('Title must be text'),
        body('content').not().isEmpty().withMessage('Content must be provided').isString().withMessage('Content must be text'),
        body('owner').not().isEmpty().withMessage('Owner of the post must be provided').isString().withMessage('Owner must be text'),
    ],
    validateRequest,
    async (req:Request, res:Response) => {

    const {title, content, owner} = req.body;        

    const post = Post.build({
        title,
        content,
        owner
    });

    await post.save();

    res.status(201).send(post)
})

export {router as newPostRouter};
Enter fullscreen mode Exit fullscreen mode

*we add a router instance to listen to a specific path on our app, in our case '/api/posts' and the method is POST
*validate request using the middleware we just created, but for the middleware to work we need to let it know what to validate, so before validateRequest function we add an array of the parameters we want to validate with the set of rules
*if not valid, again an error will be thrown and handled, if not the request will continue and reach the body of the function, where we create a post and return it to the user

inside the package.json (posts-app/posts)

...
 "scripts": {
    "start": "ts-node-dev src/index",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
...
Enter fullscreen mode Exit fullscreen mode

add the start script to use it in starting the app

Viola, that's it! Let's try it? Maybe POSTMAN is a good idea

inside the terminal (posts-app/posts)

npm run start
Enter fullscreen mode Exit fullscreen mode

Go to postman and make two requests, one is valid and one is invalid and check the response, if all goes well you should get such responses

Success request

Fail request

I hope you reached this part! This is only the start, Please in the comments, I'd like to know how you think of the article length, is it too long?? and for the way of explaining is it good or I need to adjust??

Your opinion will have an effect on the next part, I'll make sure of that!! I hope you liked this article and I hope we meet again in another article, remember keep the seat belt, the journey to the knowledge starts anytime :)

Top comments (0)