MERN Stack Patterns Skill
Comprehensive MERN stack development patterns for the keycloak-alpha multi-tenant platform with 8 microservices.
When to Use This Skill
Activate this skill when:
-
Building React + Vite frontend applications
-
Implementing Express microservices
-
Designing MongoDB schemas and data models
-
Setting up API Gateway architecture
-
Implementing session and cookie management
-
Adding error handling and validation
-
Writing tests for MERN stack applications
keycloak-alpha Project Structure
keycloak-alpha/ ├── apps/ │ ├── web-app/ # Main React + Vite SPA │ │ ├── src/ │ │ │ ├── components/ │ │ │ ├── pages/ │ │ │ ├── hooks/ │ │ │ ├── contexts/ │ │ │ ├── config/ │ │ │ ├── utils/ │ │ │ └── main.jsx │ │ ├── vite.config.js │ │ └── package.json │ └── admin-portal/ # Admin dashboard (React + Vite) │ ├── services/ │ ├── api-gateway/ # Express API Gateway │ ├── user-service/ # User management │ ├── org-service/ # Organization management │ ├── tenant-service/ # Multi-tenant provisioning │ ├── notification-service/ # Email/SMS notifications │ ├── billing-service/ # Stripe integration │ ├── analytics-service/ # Usage analytics │ └── keycloak-service/ # Keycloak integration │ ├── routes/ │ ├── api/ │ │ ├── users.js │ │ ├── organizations.js │ │ └── tenants.js │ └── index.js │ ├── shared/ │ ├── types/ │ ├── utils/ │ └── constants/ │ └── package.json
React + Vite Frontend Patterns
Vite Configuration
// apps/web-app/vite.config.js import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import path from 'path';
export default defineConfig({ plugins: [react()],
resolve: { alias: { '@': path.resolve(__dirname, './src'), '@components': path.resolve(__dirname, './src/components'), '@hooks': path.resolve(__dirname, './src/hooks'), '@contexts': path.resolve(__dirname, './src/contexts'), '@utils': path.resolve(__dirname, './src/utils'), '@config': path.resolve(__dirname, './src/config'), } },
server: { port: 3000, proxy: { '/api': { target: 'http://localhost:4000', changeOrigin: true, } } },
build: { outDir: 'dist', sourcemap: true, rollupOptions: { output: { manualChunks: { 'vendor': ['react', 'react-dom', 'react-router-dom'], 'keycloak': ['keycloak-js'], 'ui': ['@chakra-ui/react', '@emotion/react'] } } } },
optimizeDeps: { include: ['react', 'react-dom', 'keycloak-js'] } });
Component Organization
// apps/web-app/src/components/features/UserProfile/index.jsx import { useState, useEffect } from 'react'; import { useAuth } from '@hooks/useAuth'; import { useToast } from '@chakra-ui/react'; import { updateUserProfile } from '@/api/users';
export function UserProfile() { const { user } = useAuth(); const toast = useToast(); const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(false);
useEffect(() => { fetchProfile(); }, []);
async function fetchProfile() { setLoading(true); try { const data = await getUserProfile(user.sub); setProfile(data); } catch (error) { toast({ title: 'Error loading profile', description: error.message, status: 'error', duration: 5000, }); } finally { setLoading(false); } }
async function handleSubmit(formData) { try { await updateUserProfile(user.sub, formData); toast({ title: 'Profile updated', status: 'success', duration: 3000, }); } catch (error) { toast({ title: 'Update failed', description: error.message, status: 'error', duration: 5000, }); } }
if (loading) return <Spinner />; if (!profile) return <Alert>Profile not found</Alert>;
return <ProfileForm profile={profile} onSubmit={handleSubmit} />; }
Custom Hooks Pattern
// apps/web-app/src/hooks/useAuth.js import { createContext, useContext, useState, useEffect } from 'react'; import Keycloak from 'keycloak-js'; import { keycloakConfig } from '@config/keycloak.config';
const AuthContext = createContext(null);
export function AuthProvider({ children }) { const [keycloak, setKeycloak] = useState(null); const [authenticated, setAuthenticated] = useState(false); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true);
useEffect(() => { const kc = new Keycloak(keycloakConfig);
kc.init({
onLoad: 'check-sso',
checkLoginIframe: true,
pkceMethod: 'S256'
}).then(authenticated => {
setKeycloak(kc);
setAuthenticated(authenticated);
if (authenticated) {
setUser({
sub: kc.tokenParsed.sub,
email: kc.tokenParsed.email,
name: kc.tokenParsed.name,
orgId: kc.tokenParsed.org_id,
roles: kc.tokenParsed.realm_access?.roles || []
});
}
setLoading(false);
});
// Token refresh
kc.onTokenExpired = () => {
kc.updateToken(30).catch(() => {
kc.logout();
});
};
}, []);
const login = () => keycloak.login(); const logout = () => keycloak.logout(); const getToken = () => keycloak.token;
return ( <AuthContext.Provider value={{ authenticated, user, loading, login, logout, getToken }}> {children} </AuthContext.Provider> ); }
export const useAuth = () => { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within AuthProvider'); } return context; };
API Client Pattern
// apps/web-app/src/utils/apiClient.js import axios from 'axios'; import { useAuth } from '@hooks/useAuth';
const apiClient = axios.create({ baseURL: import.meta.env.VITE_API_URL || 'http://localhost:4000/api', timeout: 10000, headers: { 'Content-Type': 'application/json' } });
// Request interceptor to add auth token
apiClient.interceptors.request.use(
async (config) => {
const token = await getToken();
if (token) {
config.headers.Authorization = Bearer ${token};
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for error handling apiClient.interceptors.response.use( (response) => response.data, (error) => { if (error.response?.status === 401) { // Redirect to login window.location.href = '/login'; }
const errorMessage = error.response?.data?.message || error.message;
return Promise.reject(new Error(errorMessage));
} );
export default apiClient;
// Typed API methods
export const api = {
users: {
getProfile: (userId) => apiClient.get(/users/${userId}),
updateProfile: (userId, data) => apiClient.put(/users/${userId}, data),
listUsers: (orgId) => apiClient.get(/users?org_id=${orgId})
},
organizations: {
get: (orgId) => apiClient.get(/organizations/${orgId}),
create: (data) => apiClient.post('/organizations', data),
update: (orgId, data) => apiClient.put(/organizations/${orgId}, data)
}
};
Express Microservice Patterns
Service Structure
// services/user-service/src/index.js import express from 'express'; import helmet from 'helmet'; import cors from 'cors'; import morgan from 'morgan'; import { connectDB } from './config/database.js'; import { errorHandler } from './middleware/errorHandler.js'; import { authMiddleware } from './middleware/auth.js'; import userRoutes from './routes/users.js';
const app = express();
// Security middleware app.use(helmet()); app.use(cors({ origin: process.env.CORS_ORIGIN?.split(',') || 'http://localhost:3000', credentials: true }));
// Logging app.use(morgan('combined'));
// Body parsing app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true }));
// Connect to MongoDB await connectDB();
// Health check app.get('/health', (req, res) => { res.json({ status: 'healthy', service: 'user-service' }); });
// API routes (protected) app.use('/api/users', authMiddleware, userRoutes);
// Error handling app.use(errorHandler);
const PORT = process.env.PORT || 5001;
app.listen(PORT, () => {
console.log(User service running on port ${PORT});
});
Route Organization
// services/user-service/src/routes/users.js import express from 'express'; import { body, param, query } from 'express-validator'; import { validate } from '../middleware/validate.js'; import { requireRole } from '../middleware/rbac.js'; import * as userController from '../controllers/user.controller.js';
const router = express.Router();
// GET /api/users - List users (org admin only) router.get('/', requireRole(['org_admin']), query('org_id').optional().isString(), query('page').optional().isInt({ min: 1 }), query('limit').optional().isInt({ min: 1, max: 100 }), validate, userController.listUsers );
// GET /api/users/:id - Get user by ID router.get('/:id', param('id').isUUID(), validate, userController.getUser );
// POST /api/users - Create user (org admin only) router.post('/', requireRole(['org_admin']), body('email').isEmail().normalizeEmail(), body('firstName').trim().isLength({ min: 1, max: 50 }), body('lastName').trim().isLength({ min: 1, max: 50 }), body('orgId').isString(), validate, userController.createUser );
// PUT /api/users/:id - Update user router.put('/:id', param('id').isUUID(), body('firstName').optional().trim().isLength({ min: 1, max: 50 }), body('lastName').optional().trim().isLength({ min: 1, max: 50 }), validate, userController.updateUser );
// DELETE /api/users/:id - Delete user (org admin only) router.delete('/:id', requireRole(['org_admin']), param('id').isUUID(), validate, userController.deleteUser );
export default router;
Controller Pattern
// services/user-service/src/controllers/user.controller.js import { UserModel } from '../models/User.js'; import { KeycloakService } from '../services/keycloak.service.js'; import { AppError } from '../utils/AppError.js';
export async function listUsers(req, res, next) { try { const { org_id, page = 1, limit = 20 } = req.query;
// Ensure user can only list users from their org (unless super admin)
const orgIdFilter = req.user.roles.includes('super_admin')
? org_id
: req.user.org_id;
if (!orgIdFilter) {
throw new AppError('Organization ID required', 400);
}
const users = await UserModel.find({ org_id: orgIdFilter })
.select('-password')
.limit(limit)
.skip((page - 1) * limit)
.sort({ createdAt: -1 });
const total = await UserModel.countDocuments({ org_id: orgIdFilter });
res.json({
users,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) { next(error); } }
export async function getUser(req, res, next) { try { const { id } = req.params;
const user = await UserModel.findById(id).select('-password');
if (!user) {
throw new AppError('User not found', 404);
}
// Ensure user can only access users from their org
if (user.org_id !== req.user.org_id && !req.user.roles.includes('super_admin')) {
throw new AppError('Access denied', 403);
}
res.json(user);
} catch (error) { next(error); } }
export async function createUser(req, res, next) { try { const { email, firstName, lastName, orgId } = req.body;
// Verify org_id matches user's org (unless super admin)
if (orgId !== req.user.org_id && !req.user.roles.includes('super_admin')) {
throw new AppError('Cannot create user for different organization', 403);
}
// Create user in Keycloak
const keycloakService = new KeycloakService();
const keycloakUserId = await keycloakService.createUser({
email,
firstName,
lastName,
orgId
});
// Create user in MongoDB
const user = new UserModel({
keycloakId: keycloakUserId,
email,
firstName,
lastName,
org_id: orgId,
createdBy: req.user.sub
});
await user.save();
res.status(201).json({
id: user._id,
keycloakId: keycloakUserId,
email: user.email
});
} catch (error) { next(error); } }
MongoDB Schema Patterns
User Model
// services/user-service/src/models/User.js import mongoose from 'mongoose';
const userSchema = new mongoose.Schema({ keycloakId: { type: String, required: true, unique: true, index: true }, email: { type: String, required: true, lowercase: true, trim: true, index: true }, firstName: { type: String, required: true, trim: true }, lastName: { type: String, required: true, trim: true }, org_id: { type: String, required: true, index: true }, roles: [{ type: String, enum: ['org_admin', 'org_user', 'super_admin'] }], metadata: { type: Map, of: String }, createdBy: String, updatedBy: String }, { timestamps: true, toJSON: { transform: (doc, ret) => { ret.id = ret._id; delete ret._id; delete ret.__v; return ret; } } });
// Compound index for org queries userSchema.index({ org_id: 1, email: 1 }, { unique: true });
// Virtual for full name
userSchema.virtual('fullName').get(function() {
return ${this.firstName} ${this.lastName};
});
// Pre-save hook userSchema.pre('save', function(next) { if (this.isModified('email')) { this.email = this.email.toLowerCase(); } next(); });
export const UserModel = mongoose.model('User', userSchema);
Organization Model
// services/org-service/src/models/Organization.js import mongoose from 'mongoose';
const organizationSchema = new mongoose.Schema({ org_id: { type: String, required: true, unique: true, index: true }, name: { type: String, required: true, trim: true }, domain: { type: String, required: true, unique: true, lowercase: true }, settings: { theme: { type: String, default: 'lobbi-base' }, features: { type: Map, of: Boolean, default: new Map() }, branding: { logoUrl: String, primaryColor: String, secondaryColor: String } }, subscription: { plan: { type: String, enum: ['free', 'starter', 'professional', 'enterprise'], default: 'free' }, status: { type: String, enum: ['active', 'inactive', 'suspended'], default: 'active' }, billingCycle: { type: String, enum: ['monthly', 'annual'] }, stripeCustomerId: String, stripeSubscriptionId: String }, adminUsers: [{ userId: String, email: String, addedAt: Date }], status: { type: String, enum: ['active', 'inactive', 'suspended'], default: 'active' } }, { timestamps: true });
export const OrganizationModel = mongoose.model('Organization', organizationSchema);
API Gateway Architecture
Gateway Setup
// services/api-gateway/src/index.js import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; import { authMiddleware } from './middleware/auth.js'; import { rateLimiter } from './middleware/rateLimit.js'; import { cacheMiddleware } from './middleware/cache.js';
const app = express();
// Rate limiting app.use(rateLimiter);
// Authentication app.use(authMiddleware);
// Service routing const services = { users: process.env.USER_SERVICE_URL || 'http://localhost:5001', orgs: process.env.ORG_SERVICE_URL || 'http://localhost:5002', tenants: process.env.TENANT_SERVICE_URL || 'http://localhost:5003', notifications: process.env.NOTIFICATION_SERVICE_URL || 'http://localhost:5004', billing: process.env.BILLING_SERVICE_URL || 'http://localhost:5005', analytics: process.env.ANALYTICS_SERVICE_URL || 'http://localhost:5006' };
// Proxy to microservices
Object.entries(services).forEach(([name, target]) => {
app.use(/api/${name}, createProxyMiddleware({
target,
changeOrigin: true,
pathRewrite: { [^/api/${name}]: '' },
onProxyReq: (proxyReq, req) => {
// Forward user context
if (req.user) {
proxyReq.setHeader('X-User-Id', req.user.sub);
proxyReq.setHeader('X-Org-Id', req.user.org_id);
proxyReq.setHeader('X-User-Roles', JSON.stringify(req.user.roles));
}
}
}));
});
app.listen(4000, () => { console.log('API Gateway running on port 4000'); });
Session and Cookie Management
Session Configuration
// services/api-gateway/src/config/session.js import session from 'express-session'; import MongoStore from 'connect-mongo';
export const sessionConfig = session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false,
store: MongoStore.create({ mongoUrl: process.env.MONGODB_URL, ttl: 24 * 60 * 60, // 1 day touchAfter: 24 * 3600 // lazy session update }),
cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, maxAge: 1000 * 60 * 60 * 24, // 24 hours sameSite: 'lax', domain: process.env.COOKIE_DOMAIN },
name: 'lobbi.sid' });
Error Handling Patterns
Custom Error Classes
// shared/utils/AppError.js export class AppError extends Error { constructor(message, statusCode = 500, isOperational = true) { super(message); this.statusCode = statusCode; this.isOperational = isOperational; Error.captureStackTrace(this, this.constructor); } }
export class ValidationError extends AppError { constructor(message, errors = []) { super(message, 400); this.errors = errors; } }
export class NotFoundError extends AppError {
constructor(resource) {
super(${resource} not found, 404);
}
}
export class UnauthorizedError extends AppError { constructor(message = 'Unauthorized') { super(message, 401); } }
export class ForbiddenError extends AppError { constructor(message = 'Forbidden') { super(message, 403); } }
Error Handler Middleware
// services/user-service/src/middleware/errorHandler.js import { AppError } from '../utils/AppError.js';
export function errorHandler(err, req, res, next) { let { statusCode, message, isOperational } = err;
// Default to 500 server error if (!statusCode) { statusCode = 500; isOperational = false; }
// Log error console.error('Error:', { message, statusCode, isOperational, stack: err.stack, url: req.url, method: req.method, user: req.user?.sub });
// Send error response res.status(statusCode).json({ error: { message: isOperational ? message : 'Internal server error', statusCode, ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) } }); }
// Async error wrapper export function asyncHandler(fn) { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; }
Testing Strategies
Unit Tests with Jest
// services/user-service/tests/controllers/user.controller.test.js import { listUsers, createUser } from '../../src/controllers/user.controller.js'; import { UserModel } from '../../src/models/User.js'; import { KeycloakService } from '../../src/services/keycloak.service.js';
jest.mock('../../src/models/User.js'); jest.mock('../../src/services/keycloak.service.js');
describe('UserController', () => { describe('listUsers', () => { it('should return paginated users for org', async () => { const mockUsers = [ { _id: '1', email: 'user1@test.com', org_id: 'org_1' }, { _id: '2', email: 'user2@test.com', org_id: 'org_1' } ];
UserModel.find.mockReturnValue({
select: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
sort: jest.fn().mockResolvedValue(mockUsers)
});
UserModel.countDocuments.mockResolvedValue(2);
const req = {
query: { org_id: 'org_1', page: 1, limit: 20 },
user: { org_id: 'org_1', roles: ['org_admin'] }
};
const res = {
json: jest.fn()
};
const next = jest.fn();
await listUsers(req, res, next);
expect(res.json).toHaveBeenCalledWith({
users: mockUsers,
pagination: expect.objectContaining({
page: 1,
total: 2
})
});
});
}); });
Integration Tests
// services/user-service/tests/integration/users.test.js import request from 'supertest'; import { app } from '../../src/index.js'; import { connectDB, closeDB, clearDB } from '../setup.js';
beforeAll(async () => await connectDB()); afterEach(async () => await clearDB()); afterAll(async () => await closeDB());
describe('User API Integration', () => { it('POST /api/users - should create user', async () => { const userData = { email: 'test@example.com', firstName: 'Test', lastName: 'User', orgId: 'org_test' };
const response = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${mockAdminToken}`)
.send(userData)
.expect(201);
expect(response.body).toMatchObject({
email: userData.email
});
}); });
Best Practices
-
Use environment variables for all configuration
-
Implement proper error handling with custom error classes
-
Validate all inputs using express-validator or Joi
-
Use async/await consistently, avoid callback hell
-
Implement proper logging with structured logs
-
Use MongoDB indexes for frequently queried fields
-
Implement rate limiting to prevent abuse
-
Use CORS properly with specific origins
-
Implement request/response compression with gzip
-
Use TypeScript for better type safety (optional)
-
Implement health checks for all services
-
Use connection pooling for database connections
-
Implement graceful shutdown for services
-
Use dependency injection for better testability
-
Implement proper security headers with Helmet
File Locations in keycloak-alpha
Path Purpose
apps/web-app/
React + Vite main application
services/api-gateway/
API Gateway with routing
services/user-service/
User management microservice
services/org-service/
Organization management
routes/api/
Shared route definitions
shared/utils/
Shared utilities and helpers