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.

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?
The API Gateway is the front door. All external requests come here first.
The gateway routes requests to the correct service (e.g.,
/usersgoes to the User Service,/productsgoes to the Product Service).Each service has its own private database. This is crucial! The User Service owns the user data.
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
UserCreatedevent. 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!
Build the Docker image:
docker build -t user-service .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-serviceTest 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/usersCheck health:
curl http://localhost:5001/health
Congratulations! You just built and ran your first isolated, containerized microservice!
What We've Learned by Doing
Single Responsibility: Our User Service does one thing: manage users.
Data Ownership: It has its own database. No other service should touch this DB directly.
Independence: It can be built, deployed, and scaled independently.
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.
Follow this account on Hashnode to get the next part in this series.
Try it yourself: Clone the code from the GitHub repository for this project (create a dummy link for now).
Experiment: Can you add a
GET /api/users/:idendpoint? How would you structure it? Share your solution in the comments below!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 !!!


