DEV Community

Omar Elwakeel
Omar Elwakeel

Posted on

Microservices, express-react app (Part 2) End it with the backend!

Microservices

This is the second article 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.

Part 1

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

We previously discussed the required folder structure for the posts service, to have a complete backend we will need some authentication also, we need the user model who will be the owner of the post. I don't want this article to go long, So I will cut corners on the parts that I have covered previously.

inside user.ts (posts-app/auth/models)

import mongoose from "mongoose";

interface UserAttributes {
    username:string;
    password:string;
}

interface UserDocument extends mongoose.Document{
    username:string;
    password:string;   
}

interface UserModel extends mongoose.Model<UserDocument>{
    build(attributes:UserAttributes):UserDocument;
}

const userSchema = new mongoose.Schema({
    username:{
        type:String,
        required:true,
        unique:true
    },
    password:{
        type:String,
        required:true
    }
}, {
    toJSON:{
        transform(doc, ret){
          ret.id = ret._id;
          delete ret._id;
          delete ret.password;
          delete ret.__v;
       }
    }
})


userSchema.statics.build = (attributes:UserAttributes) => {
    return new User(attributes);
}

const User = mongoose.model<UserDocument, UserModel>('User', userSchema)

export default User;
Enter fullscreen mode Exit fullscreen mode

inside signup.ts (posts-app/auth/routes)

import express, { Request, Response } from 'express';
import { body } from 'express-validator';
import jwt from 'jsonwebtoken';
import { validateRequest } from '../middlewares/validate-request';
import { BadRequestError } from "../errors/bad-request-error";

import { User } from '../models/user';

const router = express.Router();

router.post(
  '/api/users/signup',
  [
    body('username').isString().withMessage('Username must be valid'),
    body('password')
      .trim()
      .isLength({ min: 4, max: 20 })
      .withMessage('Password must be between 4 and 20 characters'),
  ],
  validateRequest,
  async (req: Request, res: Response) => {
    const { username, password } = req.body;

    const existingUser = await User.findOne({ username });

    if (existingUser) {
      throw new BadRequestError('Username in use');
    }

    const user = User.build({ username, password });
    await user.save();

    // Generate JWT
    const userJwt = jwt.sign(
      {
        id: user.id,
        username: user.username,
      },
      process.env.JWT_KEY!
    );

    // Store it on session object
    req.session = {
      jwt: userJwt,
    };

    res.status(201).send(user);
  }
);

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

inside signin.ts (posts-app/auth/routes)

import express, { Request, Response } from 'express';
import { body } from 'express-validator';
import jwt from 'jsonwebtoken';
import { validateRequest } from '../middlewares/validate-request';
import { BadRequestError } from "../errors/bad-request-error";

import { Password } from '../services/password';
import { User } from '../models/user';

const router = express.Router();

router.post(
  '/api/users/signin',
  [
    body('email').isEmail().withMessage('Email must be valid'),
    body('password')
      .trim()
      .notEmpty()
      .withMessage('You must supply a password'),
  ],
  validateRequest,
  async (req: Request, res: Response) => {
    const { email, password } = req.body;

    const existingUser = await User.findOne({ email });
    if (!existingUser) {
      throw new BadRequestError('Invalid credentials');
    }

    const passwordsMatch = await Password.compare(
      existingUser.password,
      password
    );
    if (!passwordsMatch) {
      throw new BadRequestError('Invalid Credentials');
    }

    // Generate JWT
    const userJwt = jwt.sign(
      {
        id: existingUser.id,
        username: existingUser.username,
      },
      process.env.JWT_KEY!
    );

    // Store it on session object
    req.session = {
      jwt: userJwt,
    };

    res.status(200).send(existingUser);
  }
);

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

inside currentuser.ts (posts-app/auth/routes)

import express from 'express';
import { currentUser } from '../middlewares/current-user';

const router = express.Router();

router.get('/api/users/currentuser', currentUser, (req, res) => {
  res.send({ currentUser: req.currentUser || null });
});

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

Now with some docker as promised, in each service (auth and posts) we create a Dockerfile

inside Dockerfile (posts-app/(posts AND auth))

FROM node:alpine

WORKDIR /app
COPY package.json .
RUN npm install --only=prod
COPY . .

CMD ["npm", "start"]
Enter fullscreen mode Exit fullscreen mode

*first line we say that we need to build an image for the current service but we don't want to start from scratch, so we start it from a node js image, where alpine is the tag of that image.
*then we specify the working directory in the newly created pod to be /app.
*in the third step we only copy package.json into that directory.
*we run npm install to install the required dependencies.
*we copy everything into the working directory.
*we run the script in the package.json responsible for running the app.

we create a folder in the root directory called infra inside we create another folder called k8s which is short for Kubernetes, Kubernetes will be the master mind for the services to work all together. Inside k8s we create 5 files with extension .yaml, all as follows:

inside auth-depl.yaml (posts-app/infra/k8s)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: socialapp-auth-depl
spec:
  replicas: 1
  selector:
    matchLabels:
      app: socialapp-auth
  template:
    metadata:
      labels:
        app: socialapp-auth
    spec:
      containers:
        - name: socialapp-auth
          image: omar48/socialapp-auth
          env:
            - name: MONGO_URI
              value: 'mongodb://socialapp-auth-mongo-srv:27017/socialapp-auth'
            - name: JWT_KEY
              valueFrom:
                secretKeyRef:
                  name: jwt-secret
                  key: JWT_KEY
---
apiVersion: v1
kind: Service
metadata:
  name: socialapp-auth-srv
spec:
  selector:
    app: socialapp-auth
  ports:
    - name: socialapp-auth
      protocol: TCP
      port: 3000
      targetPort: 3000

Enter fullscreen mode Exit fullscreen mode

*we specify the purpose of this configuration which will be a deployment, its name and the environment variables required as mentioned the above snippet of code.
*The deployment needs a network service for the requests incoming, we make it in the same file, separated by the three hyphens in the middle of the file. The type is service, we specify the name and the port numbers it will listen to.

inside auth-mongo-depl.yaml (posts-app/infra/k8s)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: socialapp-auth-mongo-depl
spec:
  replicas: 1
  selector:
    matchLabels:
      app: socialapp-auth-mongo
  template:
    metadata:
      labels:
        app: socialapp-auth-mongo
    spec:
      containers:
        - name: socialapp-auth-mongo
          image: mongo
---
apiVersion: v1
kind: Service
metadata:
  name: socialapp-auth-mongo-srv
spec:
  selector:
    app: socialapp-auth-mongo
  ports:
    - name: db
      protocol: TCP
      port: 27017
      targetPort: 27017
Enter fullscreen mode Exit fullscreen mode

*Microservices separates services, and puts them to work each on its own, considering the database as well. so we make a deployment for the db also for each service, we also specify the port in the service, the default for mongo is 27017

inside posts-depl.yaml (posts-app/infra/k8s)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: socialapp-posts-depl
spec:
  replicas: 1
  selector:
    matchLabels:
      app: socialapp-posts
  template:
    metadata:
      labels:
        app: socialapp-posts
    spec:
      containers:
        - name: socialapp-posts
          image: omar48/socialapp-posts
          env:
            - name: NATS_CLIENT_ID
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: NATS_URL
              value: 'http://nats-srv:4222'
            - name: NATS_CLUSTER_ID
              value: ticketing
            - name: MONGO_URI
              value: 'mongodb://socialapp-posts-mongo-srv:27017/socialapp-posts'
            - name: JWT_KEY
              valueFrom:
                secretKeyRef:
                  name: jwt-secret
                  key: JWT_KEY
---
apiVersion: v1
kind: Service
metadata:
  name: socialapp-posts-srv
spec:
  selector:
    app: socialapp-posts
  ports:
    - name: socialapp-posts
      protocol: TCP
      port: 3000
      targetPort: 3000
Enter fullscreen mode Exit fullscreen mode

inside posts-mongo-depl.yaml (posts-app/infra/k8s)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: socialapp-posts-mongo-depl
spec:
  replicas: 1
  selector:
    matchLabels:
      app: socialapp-posts-mongo
  template:
    metadata:
      labels:
        app: socialapp-posts-mongo
    spec:
      containers:
        - name: socialapp-posts-mongo
          image: mongo
---
apiVersion: v1
kind: Service
metadata:
  name: socialapp-posts-mongo-srv
spec:
  selector:
    app: socialapp-posts-mongo
  ports:
    - name: db
      protocol: TCP
      port: 27017
      targetPort: 27017
Enter fullscreen mode Exit fullscreen mode

For these services to communicate together we need to communication master mind to know about them, who is the master mind? Nginx so the fifth configuration file will be for ingress-nginx

inside ingress-srv.yaml (posts-app/infra/k8s)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-service
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/use-regex: 'true'
spec:
  rules:
    - host: posts.dev
      http:
        paths:
          - path: /api/posts/?(.*)
            pathType: Prefix
            backend:
              service:
                name: socialapp-posts-srv
                port: 
                  number: 3000
          - path: /api/users/?(.*)
            pathType: Prefix
            backend:
              service:
                name: socialapp-auth-srv
                port: 
                  number: 3000
Enter fullscreen mode Exit fullscreen mode

We are almost done, the last thing to do is the config file that will listen to any change that will take place inside our app.

inside skaffold.yaml (posts-app)

apiVersion: skaffold/v2alpha3
kind: Config
deploy:
  kubectl:
    manifests:
      - ./infra/k8s/*
build:
  local:
    push: false
  artifacts:
    - image: omar48/socialapp-auth
      context: auth
      docker:
        dockerfile: Dockerfile
      sync:
        manual:
          - src: 'src/**/*.ts'
            dest: .
    - image: omar48/socialapp-posts
      context: posts
      docker:
        dockerfile: Dockerfile
      sync:
        manual:
          - src: 'src/**/*.ts'
            dest: .
Enter fullscreen mode Exit fullscreen mode

Now we can give it a try, inside the root directory run skaffold dev

I hope it's working with you, in the next article, I will build the client app that will make use of this backend logic, then will use a service called NATS streaming service to communicate between services.

Top comments (0)