Skip to main content

Command Palette

Search for a command to run...

Microservices Hands-On: Building Your First Service & Understanding the Why

Theory is good, but building is better. Let's code our first microservice and learn the core concepts by doing.

Published
6 min read
Microservices Hands-On: Building Your First Service & Understanding the Why

Welcome to Learning by Doing!

If you're reading this, you're probably tired of just reading about microservices. You've seen the fancy diagrams and heard all the buzzwords—"scalability," "resilience," "agility." But how does it all actually work? How do you go from a single server.js file to a fleet of independent services?

That's what this blog is all about. We learn by getting our hands dirty with code.

In this first post, we won't build a massive system. We'll do something much more valuable: we'll build a single, production-ready microservice for user management. We'll understand its internal structure, how it's containerized, and how it would communicate in a larger ecosystem.

By the end, you won't just know what a microservice is; you'll have built one.

The "Why" Before the "How"

Before we write a line of code, let's visualize our goal. We're not building a monolith. We're building one piece of a larger puzzle.

Sample Architecture Diagram:

What's happening here?

  1. The API Gateway is the front door. All external requests come here first.

  2. The gateway routes requests to the correct service (e.g., /users goes to the User Service, /products goes to the Product Service).

  3. Each service has its own private database. This is crucial! The User Service owns the user data.

  4. Services communicate asynchronously via events using a Message Broker (like Redis or RabbitMQ). For example, when a new user signs up, the User Service publishes a UserCreated event. The Product Service (or any other service) can listen and react.

Today, we are building the User Service (the blue one).

Hands-On: Building the User Service

Let's create our service. We'll use Node.js with Express for its simplicity, but the principles apply to any language.

Step 1: Project Structure

A clear structure is the foundation of a maintainable service. Here's what we'll create:

user-service/
├── src/
│   ├── controllers/
│   │   └── userController.js
│   ├── models/
│   │   └── User.js
│   ├── routes/
│   │   └── userRoutes.js
│   ├── config/
│   │   └── database.js
│   └── app.js
├── tests/
│   └── user.test.js
├── Dockerfile
├── .dockerignore
├── package.json
└── README.md

Step 2: The Core Application Code

First, package.json:

{
  "name": "user-service",
  "version": "1.0.0",
  "description": "A simple user management microservice",
  "main": "src/app.js",
  "scripts": {
    "start": "node src/app.js",
    "dev": "nodemon src/app.js",
    "test": "jest"
  },
  "dependencies": {
    "express": "^4.18.2",
    "mongoose": "^7.4.0",
    "dotenv": "^16.3.1"
  },
  "devDependencies": {
    "nodemon": "^3.0.1",
    "jest": "^29.6.1",
    "supertest": "^6.3.3"
  }
}

Next, our data model src/models/User.js. We use Mongoose for ODM.

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    trim: true
  },
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true
  },
  password: { // In a real app, this would be hashed!
    type: String,
    required: true,
    minlength: 6
  }
}, {
  timestamps: true // Adds createdAt and updatedAt
});

module.exports = mongoose.model('User', userSchema);

Now, the controller src/controllers/userController.js to handle the logic.

const User = require('../models/User');

// @desc    Create a new user
// @route   POST /api/users
// @access  Public
const createUser = async (req, res) => {
  try {
    const user = new User(req.body);
    await user.save();

    // TODO: In Part 2, we will publish a "UserCreated" event here!
    // messageBroker.publish('USER_CREATED', user);

    res.status(201).json({
      success: true,
      data: user
    });
  } catch (error) {
    if (error.code === 11000) {
      return res.status(400).json({
        success: false,
        message: 'Email already exists'
      });
    }
    res.status(500).json({
      success: false,
      message: 'Server Error'
    });
  }
};

// @desc    Get all users
// @route   GET /api/users
// @access  Public
const getUsers = async (req, res) => {
  try {
    const users = await User.find();
    res.status(200).json({
      success: true,
      count: users.length,
      data: users
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      message: 'Server Error'
    });
  }
};

module.exports = {
  createUser,
  getUsers
};

Let's define the routes in src/routes/userRoutes.js:

const express = require('express');
const { createUser, getUsers } = require('../controllers/userController');

const router = express.Router();

router.route('/')
  .post(createUser)
  .get(getUsers);

module.exports = router;

Finally, let's tie it all together in src/app.js:

require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const userRoutes = require('./routes/userRoutes');

const app = express();

// Connect to Database
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/userdb', {
  useNewUrlParser: true,
  useUnifiedTopology: true
}).then(() => console.log('User Service DB Connected'))
  .catch(err => console.log(err));

// Middleware
app.use(express.json());

// Routes
app.use('/api/users', userRoutes);

// Basic health check
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'UP', service: 'User Service' });
});

const PORT = process.env.PORT || 5001;
app.listen(PORT, () => console.log(`User service running on port ${PORT}`));

Step 3: Containerizing with Docker

A microservice isn't truly independent if it can't run anywhere. We use Docker.

Create a Dockerfile:

# Use a specific Node.js version for stability
FROM node:18-alpine

# Set the working directory inside the container
WORKDIR /app

# Copy package files first to leverage Docker cache
COPY package*.json ./

# Install dependencies
RUN npm install --only=production

# Copy the rest of the application code
COPY . .

# Expose the port the app runs on
EXPOSE 5001

# Define the command to run the application
CMD ["npm", "start"]

And a .dockerignore file:

node_modules
npm-debug.log
.git
.gitignore
README.md
.env

Let's Run It!

  1. Build the Docker image:

     docker build -t user-service .
    
  2. Run the container: (We'll connect it to a MongoDB container)

     # First, create a network for our future services to communicate
     docker network create microservices-net
    
     # Run MongoDB
     docker run -d --name mongodb -p 27017:27017 --network microservices-net mongo:latest
    
     # Run our User Service, connected to the database and exposing port 5001
     docker run -d --name user-service -p 5001:5001 --network microservices-net -e MONGODB_URI=mongodb://mongodb:27017/userdb user-service
    
  3. Test it!

    • Create a user:

        curl -X POST http://localhost:5001/api/users \
        -H "Content-Type: application/json" \
        -d '{"name":"John Doe", "email":"john@doe.com", "password":"password123"}'
      
    • Get all users:

        curl http://localhost:5001/api/users
      
    • Check health:

        curl http://localhost:5001/health
      

Congratulations! You just built and ran your first isolated, containerized microservice!

What We've Learned by Doing

  1. Single Responsibility: Our User Service does one thing: manage users.

  2. Data Ownership: It has its own database. No other service should touch this DB directly.

  3. Independence: It can be built, deployed, and scaled independently.

  4. Containerization: Docker makes it portable and predictable.

What's Next?

This is just the beginning. In the next post, we'll:

  • Build the Product Service.

  • Add a Message Broker (Redis).

  • Make our services communicate by publishing and consuming events when a user is created.

We'll see how the Product Service can react to the UserCreated event without the User Service even knowing it exists. That's the power of loose coupling!


Call to Action

The best way to learn is to do, and then to discuss.

  1. Follow this account on Hashnode to get the next part in this series.

  2. Try it yourself: Clone the code from the GitHub repository for this project (create a dummy link for now).

  3. Experiment: Can you add a GET /api/users/:id endpoint? How would you structure it? Share your solution in the comments below!

  4. Question for you: What's the one thing about microservices that still confuses you the most? Let me know, and we might build a solution for it in a future post.

Lets learn by doing it !!!

L

Great breakdown of building a microservices architecture from scratch! The step-by-step approach, from setting up the Post, Comment, and Query services to implementing an event bus for communication, provides a comprehensive guide for developers. For local development, tools like ServBay can simplify the setup process, allowing you to focus more on coding and less on environment configuration. Definitely worth checking out if you're looking to streamline your dev workflow.

1
D
Deva Kumar8mo ago

Thanks for this