Post 10 of 15 | Phase 6: API Gateway
API Gateway — Exposing Your Services to the Outside World
In every post so far, your services have been talking to each other internally. No browser, no Postman, no mobile app can reach them directly. They are completely internal. The API Gateway is the single entry point that sits between the outside world and your internal services. It receives HTTP requests, translates them into Moleculer action calls, and sends the response back.
What is an API Gateway in Microservices
In a monolith, every route is directly accessible. In microservices, internal services should never be exposed directly to the internet. You expose one single service — the API Gateway — and everything else stays private.
Without API Gateway (wrong):
Browser → user-service:3001
Browser → order-service:3002
Browser → product-service:3003
All services exposed publicly. Security nightmare.
With API Gateway (correct):
Browser → API Gateway:3000 → user-service (internal)
→ order-service (internal)
→ product-service (internal)
Only one public entry point. Everything else is private.
The API Gateway handles:
- Receiving HTTP requests from clients
- Authentication and authorization
- Request validation
- Routing to the correct internal service
- Sending the response back to the client
moleculer-web — The Official API Gateway
Moleculer has an official API Gateway package called moleculer-web. It is itself a Moleculer service. It runs inside your broker alongside other services and automatically maps HTTP routes to Moleculer actions.
You already have it in your project because you selected it during the CLI setup. Let us go through it properly now.
Check your package.json — moleculer-web should already be there. If not, install it:
npm install moleculer-web
The api.service.js File — Full Breakdown
Open services/api.service.js. The CLI generated a basic version. Let us rewrite it completely so you understand every part:
"use strict";
const ApiGateway = require("moleculer-web");
module.exports = {
name: "api",
// moleculer-web is used as a mixin
// A mixin merges another service schema into this one
// This gives your api service all the HTTP server capabilities
mixins: [ApiGateway],
settings: {
// Port the HTTP server listens on
port: process.env.PORT || 3000,
// IP address to bind to
// 0.0.0.0 means accept connections from any IP
ip: "0.0.0.0",
// Routes define how HTTP requests map to Moleculer actions
routes: [
{
// All routes in this block start with /api
path: "/api",
// Enable whitelist — only listed actions are accessible
// Use "**" to allow all actions
whitelist: [
"**"
],
// Automatically map REST routes from action definitions
// This reads the rest property from each action
// and creates the corresponding HTTP route
autoAliases: true,
// Body parser — needed for POST/PUT requests with JSON body
bodyParsers: {
json: { strict: false, limit: "1MB" },
urlencoded: { extended: true, limit: "1MB" }
},
// CORS configuration
cors: {
origin: "*",
methods: ["GET", "OPTIONS", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: false
},
// Hooks run before and after every request in this route block
onBeforeCall(ctx, route, req, res) {
// This runs before every action call
// Good place to extract auth token and put in ctx.meta
ctx.meta.clientIP = req.headers["x-forwarded-for"] || req.socket.remoteAddress;
ctx.meta.userAgent = req.headers["user-agent"];
},
onAfterCall(ctx, route, req, res, data) {
// This runs after every action call
// data is the result returned by the action
// You can modify data here before sending to client
return data;
}
}
],
// Global error handler
onError(req, res, err) {
res.setHeader("Content-Type", "application/json");
res.writeHead(err.code || 500);
res.end(JSON.stringify({
error: true,
message: err.message,
code: err.code || 500
}));
}
}
};
How autoAliases Works
When autoAliases: true is set, the API Gateway reads the rest property from every action in every service and automatically creates the corresponding HTTP route.
Your user service has:
actions: {
list: {
rest: { method: "GET", path: "/" },
handler(ctx) { ... }
},
getById: {
rest: { method: "GET", path: "/:id" },
handler(ctx) { ... }
},
create: {
rest: { method: "POST", path: "/" },
handler(ctx) { ... }
}
}
With autoAliases, these become:
GET /api/user → user.list
GET /api/user/:id → user.getById
POST /api/user → user.create
The URL pattern is: /api/serviceName/actionPath
This is automatic. You define the rest property in your service, and the API Gateway picks it up without any additional configuration.
Manual Aliases — Custom URL Mapping
Sometimes you want custom URLs that do not follow the automatic pattern. Use the aliases property:
routes: [
{
path: "/api",
aliases: {
// Custom URL → action mapping
// Format: "METHOD URL": "service.action"
"POST /auth/login": "user.login",
"POST /auth/register": "user.register",
"POST /auth/logout": "user.logout",
// REST shorthand — maps all CRUD routes automatically
// GET /api/products → product.list
// GET /api/products/:id → product.getById
// POST /api/products → product.create
// PUT /api/products/:id → product.update
// DELETE /api/products/:id → product.remove
"REST /products": "product",
// Custom name for a route
"GET /me": "user.getProfile",
// Multi-word service names use dot notation
"GET /admin/stats": "admin.getStats"
},
// When using manual aliases, set autoAliases to false
// or use both together
autoAliases: true
}
]
Authentication — The Most Important Part
This is where the real world gets serious. Your API Gateway must verify who is making each request before passing it to your services. The standard approach is JWT authentication.
Here is the complete pattern:
"use strict";
const ApiGateway = require("moleculer-web");
const jwt = require("jsonwebtoken");
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
module.exports = {
name: "api",
mixins: [ApiGateway],
settings: {
port: 3000,
routes: [
// Route 1: Public routes — no authentication needed
{
path: "/api/public",
whitelist: [
"user.login",
"user.register"
],
bodyParsers: {
json: true,
urlencoded: { extended: true }
},
aliases: {
"POST /login": "user.login",
"POST /register": "user.register"
}
},
// Route 2: Protected routes — authentication required
{
path: "/api",
whitelist: ["**"],
autoAliases: true,
bodyParsers: {
json: true,
urlencoded: { extended: true }
},
// authenticate runs before every request in this route block
// Return the decoded user object to allow the request
// Throw an error to reject the request
async authenticate(ctx, route, req) {
const authHeader = req.headers["authorization"];
if (!authHeader) {
throw new ApiGateway.Errors.UnAuthorizedError(
ApiGateway.Errors.ERR_NO_TOKEN
);
}
// Header format: "Bearer <token>"
const token = authHeader.split(" ")[1];
if (!token) {
throw new ApiGateway.Errors.UnAuthorizedError(
ApiGateway.Errors.ERR_NO_TOKEN
);
}
try {
// Verify the JWT token
const decoded = jwt.verify(token, JWT_SECRET);
// Return the user — this gets stored in ctx.meta.user
// and is available in all downstream services
return decoded;
} catch (err) {
throw new ApiGateway.Errors.UnAuthorizedError(
ApiGateway.Errors.ERR_INVALID_TOKEN
);
}
},
// authorize runs after authenticate
// Here you check if the authenticated user has
// permission to access the requested action
async authorize(ctx, route, req) {
const user = ctx.meta.user;
// Example: only admins can access admin actions
if (req.$action.name.startsWith("admin.")) {
if (!user || user.role !== "admin") {
throw new ApiGateway.Errors.ForbiddenError(
"Admin access required"
);
}
}
}
}
]
}
};
Install jsonwebtoken:
npm install jsonwebtoken
Now update your user service to generate a JWT on login:
"use strict";
const jwt = require("jsonwebtoken");
const { MoleculerClientError } = require("moleculer").Errors;
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
const users = [
{ id: "1", name: "Rahul Sharma", email: "rahul@example.com", password: "password123", role: "admin" },
{ id: "2", name: "Priya Singh", email: "priya@example.com", password: "password456", role: "user" }
];
module.exports = {
name: "user",
actions: {
// Public action — no auth needed
login: {
params: {
email: "email",
password: "string"
},
handler(ctx) {
const user = users.find(u =>
u.email === ctx.params.email &&
u.password === ctx.params.password
);
if (!user) {
throw new MoleculerClientError(
"Invalid email or password",
401,
"INVALID_CREDENTIALS"
);
}
// Generate JWT token
const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: "24h" }
);
// Put token in ctx.meta
// API Gateway will read it from here
ctx.meta.token = token;
return {
message: "Login successful",
user: { id: user.id, name: user.name, email: user.email, role: user.role }
};
}
},
// Public action — no auth needed
register: {
params: {
name: "string",
email: "email",
password: { type: "string", min: 6 }
},
handler(ctx) {
const exists = users.find(u => u.email === ctx.params.email);
if (exists) {
throw new MoleculerClientError(
"Email already registered",
422,
"EMAIL_EXISTS"
);
}
const newUser = {
id: String(users.length + 1),
name: ctx.params.name,
email: ctx.params.email,
password: ctx.params.password,
role: "user"
};
users.push(newUser);
return {
message: "Registration successful",
user: { id: newUser.id, name: newUser.name, email: newUser.email }
};
}
},
// Protected action — requires authentication
getProfile: {
rest: { method: "GET", path: "/profile" },
handler(ctx) {
// ctx.meta.user is set by the API Gateway authenticate hook
const currentUser = ctx.meta.user;
const user = users.find(u => u.id === currentUser.id);
if (!user) {
throw new MoleculerClientError("User not found", 404, "NOT_FOUND");
}
return {
id: user.id,
name: user.name,
email: user.email,
role: user.role
};
}
},
// Protected action — list all users
list: {
rest: { method: "GET", path: "/" },
handler(ctx) {
return users.map(u => ({
id: u.id,
name: u.name,
email: u.email,
role: u.role
}));
}
}
}
};
Sending the JWT Token Back to the Client
When the user logs in, the token is in ctx.meta.token. You need to send it to the client in the response. Add an onAfterCall hook to the public route:
// In the public route block
onAfterCall(ctx, route, req, res, data) {
// If a token was set in meta during the action
// send it as a response header
if (ctx.meta.token) {
res.setHeader("Authorization", `Bearer ${ctx.meta.token}`);
}
return data;
}
Now the client receives the token in the Authorization response header. They store it and send it with every subsequent request.
Testing the Full Auth Flow in Postman
Step 1: Register
POST http://localhost:3000/api/public/register
Body: {
"name": "Amit Kumar",
"email": "amit@example.com",
"password": "secret123"
}
Step 2: Login
POST http://localhost:3000/api/public/login
Body: {
"email": "amit@example.com",
"password": "secret123"
}
Copy the token from the Authorization response header.
Step 3: Access protected route
GET http://localhost:3000/api/user/profile
Headers: Authorization: Bearer <paste token here>
You get the profile. Remove the token and try again — you get a 401 Unauthorized error.
Whitelist — Controlling Which Actions Are Accessible
The whitelist controls which Moleculer actions the API Gateway exposes. This is a security feature. Actions not in the whitelist return a 404.
routes: [
{
path: "/api",
whitelist: [
// Allow specific actions
"user.list",
"user.getById",
"product.list",
"product.getById",
// Allow all actions of a service
"order.*",
// Allow everything (use carefully)
"**"
]
}
]
In production, never use "**" on authenticated routes without thinking. Be explicit about what you expose.
Global Rate Limiting
You can add basic rate limiting to prevent abuse:
routes: [
{
path: "/api",
rateLimit: {
// Window size in milliseconds
window: 60 * 1000, // 1 minute
// Max requests per window per IP
limit: 100,
// Response when limit exceeded
headers: true,
key: (req) => {
return req.headers["x-forwarded-for"] ||
req.socket.remoteAddress;
}
}
}
]
Accessing URL Parameters
When you define a route with a parameter like /:id, Moleculer automatically merges it into ctx.params:
// Route definition in action
rest: { method: "GET", path: "/:id" }
// Request: GET /api/user/123
// Inside handler:
handler(ctx) {
console.log(ctx.params.id); // "123"
}
Query string parameters also merge automatically:
GET /api/user?page=2&limit=10
handler(ctx) {
console.log(ctx.params.page); // "2"
console.log(ctx.params.limit); // "10"
}
Complete Folder Structure at This Point
Your project should now look like this:
my-project/
services/
api.service.js ← API Gateway with auth
user.service.js ← User service with login, register, profile
product.service.js ← Product service with caching
order.service.js ← Order service with events
email.service.js ← Email service listening to events
notification.service.js ← Notification service
moleculer.config.js
package.json
You now have a real microservices backend taking shape.
Summary
- The API Gateway is the single public entry point. Internal services are never exposed directly.
- moleculer-web is the official API Gateway package. It runs as a Moleculer service using a mixin.
- autoAliases: true automatically creates HTTP routes from the rest property in each action.
- Manual aliases give you full control over URL to action mapping.
- Two route blocks: public routes with no auth, protected routes with authentication.
- The authenticate hook verifies the JWT token and returns the decoded user which goes into ctx.meta.user.
- The authorize hook checks if the authenticated user has permission for the requested action.
- onBeforeCall and onAfterCall hooks run before and after every request.
- Use onAfterCall to send the JWT token back to the client in the response header.
- Whitelist controls which actions are accessible through the gateway.
- URL parameters and query strings are automatically merged into ctx.params.
- Rate limiting is built-in and configured per route block.
Up Next
Post 11 covers Database Integration using moleculer-db with MongoDB and Mongoose. We will replace the in-memory arrays in our services with real database operations, and you will see how moleculer-db gives you free CRUD actions out of the box with zero boilerplate.
Course Progress: 10 of 15 posts complete.
No comments:
Post a Comment