mern-patterns

MERN Stack Patterns Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "mern-patterns" with this command: npx skills add lobbi-docs/claude/lobbi-docs-claude-mern-patterns

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 &#x26;&#x26; !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 &#x26;&#x26; !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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

vision-multimodal

No summary provided by upstream source.

Repository SourceNeeds Review
General

design-system

No summary provided by upstream source.

Repository SourceNeeds Review
General

kanban

No summary provided by upstream source.

Repository SourceNeeds Review
General

gcp

No summary provided by upstream source.

Repository SourceNeeds Review