mern-patterns
SKILL.md
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 |
Weekly Installs
6
Repository
lobbi-docs/claudeGitHub Stars
9
First Seen
Feb 27, 2026
Security Audits
Installed on
opencode6
github-copilot6
codex6
kimi-cli6
gemini-cli6
cursor6