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!!
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.
let's setup our app structure
inside the root directory terminal (posts-app)
git init
to initialize a git repository, after a while we will connect to our github account
inside the terminal (posts-app/posts)
npm init --y
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
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
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
inside .env (posts-app/posts)
PORT=3000
MONGO_URI=mongodb://localhost:27017/posts-app
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;
*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();
*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;
*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"}]
})
}
*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();
}
*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}[];
}
*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 }
})
}
}
*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};
*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"
},
...
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
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
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)