Table of contents
No headings in the article.
In early software development, the best practice to develop a software application is by using a monolithic architecture. However, monolithic applications require extra effort to respond to changes in the system. If any particular component develops a fault, the entire system is affected due to the high coupling.
Nowadays, we can solve this problem by using the power of Microservices to develop scalable and flexible software systems. In this article, we'll explore how to use microservices with Express & Typescript, to build a system that includes two services: user service, and order management service, and call them by an API gateway (I'll use express-gateway in this article).
Before diving into the specifics of building the system, let's briefly define microservices.
Microservices are small, independent services that work together to provide a larger application. Each microservice focuses on a specific functionality, such as user authentication or order processing. By breaking the application down into smaller services, it becomes easier to scale and maintain.
Now, let's discuss how to build our services using Express.
To stay focused on the main topic and keep the article concise, I'll only include brief "hello world" demos and avoid delving too deeply into the express code.
- The user service will handle user authentication. It will be responsible for creating new users in our MongoDB database.
To build the user service, we'll start by creating a new Express application and installing the required dependencies.
mkdir express-gateway && cd express-gateway && mkdir user-service && cd user-service && touch index.ts
pnpm init
pnpm i express mongoose
pnpm i -D @types/node @types/express typescript eslint nodemon ts-node dotenv
now let's add our endpoints, but before we need to change our package.json configurations and add our .env and tsconfig.json files.
in our package.json we need to add the "start":"nodemon" scripts
"scripts": {
"start": "nodemon"
},
add tsconfig.json file in the root directory and put the following on it
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"esModuleInterop": true,
"noImplicitAny": true,
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"baseUrl": ".",
"paths": {
"*": [
"node_modules/*",
]
}
},
"include": [
"index.ts",
]
}
add .env file in the root directory
DATABASE_URL='PUT_YOUR_DATABASE_URL_HERE'
PORT=4000
now we can work on our index.ts file
import express from 'express';
import { connect } from "mongoose";
import dotenv from 'dotenv';
dotenv.config();
const app = express();
app.use('/', (req, res) => {
res.send('Hello World from user service');
});
connect(
process.env.DATABASE_URL || '',
).then(() => {
console.log("Database connected..");
});
app.listen(process.env.PORT, () =>
console.log(`server running on port : ${process.env.PORT} \n`)
);
export default app;
- The order management service will handle order processing. It will be responsible for creating new orders, updating order status, and generating invoices.
To build the order management service, we'll once again start by creating a new Express application.
so let's do that quickly, I'll just mention files that have changes ( make sure to change your directory to express-gateway before)
mkdir order-service && cd order-service && touch index.ts
pnpm init
pnpm i express mongoose
pnpm i -D @types/node @types/express typescript eslint nodemon ts-node dotenv
we can use a different database in this separate service, and that's another advantage of using a microservice.
and change the port to not override the user-service port
DATABASE_URL='PUT_YOUR_DATABASE_URL_HERE'
PORT=4001
import express from 'express';
import { connect } from "mongoose";
import dotenv from 'dotenv';
dotenv.config();
const app = express();
app.use('/', (req, res) => {
res.send('Hello World from order service');
});
connect(
process.env.DATABASE_URL || '',
).then(() => {
console.log("Database connected..");
});
app.listen(process.env.PORT, () =>
console.log(`server running on port : ${process.env.PORT} \n`)
);
export default app;
now we need to test our services independently to make sure they're working before integrating them with our gateway. We just need to run "pnpm start" since we used pnpm as a package manager.
Now that we've built our services, and confirmed that they are working, we need to integrate them together. One way to do this is by using an API gateway, which is express-gateway in our case. An API gateway acts as a single entry point for all the microservices in the system. It handles authentication, routing, and load balancing.
To use an API gateway with our services, we'll need to define the routes for each service in the gateway configuration.
let's start creating the express-gateway app before
pnpm install -g express-gateway
eg gateway create
cd my-gateway && pnpm start
we just need to do some changes under config/gateway.config.yml by adding the apiEndpoints and serviceEndpoints and specifying the pipelines.
http:
port: 8080
admin:
port: 9876
host: localhost
apiEndpoints:
user:
host: localhost
paths: ['/user', '/user/*']
order:
host: localhost
paths: ['/order', '/order/*']
serviceEndpoints:
userService:
url: 'http://localhost:4000'
orderService:
url: 'http://localhost:4001'
policies:
- basic-auth
- cors
- expression
- key-auth
- log
- oauth2
- proxy
- rate-limit
pipelines:
userPipeline:
apiEndpoints:
- user
policies:
- proxy:
- action:
serviceEndpoint: userService
changeOrigin: true
orderPipeline:
apiEndpoints:
- order
policies:
- proxy:
- action:
serviceEndpoint: orderService
changeOrigin: true
Once we've integrated our services with the API gateway, we can start testing the system. We can use tools like Postman or Curl (I will use Curl here since I prefer command-line interfaces over GUIs) to send requests to the API gateway and verify that the services are working correctly.
after we run each microservice separately, we'll run the gateway and request the server on port 8080 which will redirect to the specific microservice depending on the request endpoint.
and voila, it works
Finally, it's worth noting that microservices can introduce additional complexity and overhead compared to a monolithic application. While microservices can provide significant benefits in terms of scalability and flexibility, they can also require more development effort and operational overhead.
In summary, microservices with Express and an API gateway can be a powerful approach to building scalable and flexible software systems. By breaking down your application into smaller services, you can focus on specific functionalities and make your system easier to maintain.
๐ค I hope you enjoy reading this article! ๐ You can find all the code in this repo , so feel free to explore and happy coding! ๐ป