Microservices, express-react app (Part 2) End it with the backend!
Omar Elwakeel
Posted on November 20, 2021
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.
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;
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 };
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 };
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 };
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"]
*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
*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
*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
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
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
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: .
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.
Posted on November 20, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.