skills/lobbi-docs/claude/mern-patterns

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

  1. Use environment variables for all configuration
  2. Implement proper error handling with custom error classes
  3. Validate all inputs using express-validator or Joi
  4. Use async/await consistently, avoid callback hell
  5. Implement proper logging with structured logs
  6. Use MongoDB indexes for frequently queried fields
  7. Implement rate limiting to prevent abuse
  8. Use CORS properly with specific origins
  9. Implement request/response compression with gzip
  10. Use TypeScript for better type safety (optional)
  11. Implement health checks for all services
  12. Use connection pooling for database connections
  13. Implement graceful shutdown for services
  14. Use dependency injection for better testability
  15. 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
GitHub Stars
9
First Seen
Feb 27, 2026
Installed on
opencode6
github-copilot6
codex6
kimi-cli6
gemini-cli6
cursor6