Skip to main content

Controller Architecture

Controllers in LibreChat handle business logic, coordinate between services and models, and manage request/response flow. They keep routes thin and maintainable.

Controller Organization

Controllers are organized in api/server/controllers/:
api/server/controllers/
├── AuthController.js
├── UserController.js
├── ModelController.js
├── EndpointController.js
├── PluginController.js
├── PermissionsController.js
├── auth/
│   ├── LoginController.js
│   └── LogoutController.js
└── ...

Controller Patterns

Basic Controller Structure

Controllers are exported functions that handle specific actions:
const { logger } = require('@librechat/data-schemas');
const { findUser, updateUser } = require('~/models');

const getUserController = async (req, res) => {
  try {
    const userData = req.user.toObject();
    
    // Remove sensitive fields
    delete userData.password;
    delete userData.totpSecret;
    
    res.status(200).send(userData);
  } catch (error) {
    logger.error('[getUserController]', error);
    res.status(500).json({ message: 'Error fetching user data' });
  }
};

module.exports = { getUserController };

Authentication Controllers

Registration Controller

File: api/server/controllers/AuthController.js
const bcrypt = require('bcryptjs');
const { registerUser } = require('~/server/services/AuthService');
const { logger } = require('@librechat/data-schemas');

const registrationController = async (req, res) => {
  try {
    const response = await registerUser(req.body);
    const { status, message } = response;
    res.status(status).send({ message });
  } catch (err) {
    logger.error('[registrationController]', err);
    return res.status(500).json({ message: err.message });
  }
};

Login Controller

Handles authentication with session management:
const { setAuthTokens } = require('~/server/services/AuthService');

const loginController = async (req, res) => {
  try {
    const user = req.user;
    const token = await setAuthTokens(user._id, res);
    
    res.status(200).send({ token, user });
  } catch (error) {
    logger.error('[loginController]', error);
    res.status(500).json({ message: 'Login failed' });
  }
};

Password Reset Controllers

const { requestPasswordReset, resetPassword } = require('~/server/services/AuthService');

const resetPasswordRequestController = async (req, res) => {
  try {
    const resetService = await requestPasswordReset(req);
    if (resetService instanceof Error) {
      return res.status(400).json(resetService);
    }
    return res.status(200).json(resetService);
  } catch (e) {
    logger.error('[resetPasswordRequestController]', e);
    return res.status(400).json({ message: e.message });
  }
};

const resetPasswordController = async (req, res) => {
  try {
    const resetPasswordService = await resetPassword(
      req.body.userId,
      req.body.token,
      req.body.password,
    );
    
    if (resetPasswordService instanceof Error) {
      return res.status(400).json(resetPasswordService);
    }
    
    return res.status(200).json(resetPasswordService);
  } catch (e) {
    logger.error('[resetPasswordController]', e);
    return res.status(400).json({ message: e.message });
  }
};

Token Refresh Controller

const jwt = require('jsonwebtoken');
const { getUserById, findSession } = require('~/models');

const refreshController = async (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  
  if (!refreshToken) {
    return res.status(200).send('Refresh token not provided');
  }
  
  try {
    const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
    const user = await getUserById(payload.id, '-password -__v');
    
    if (!user) {
      return res.status(401).redirect('/login');
    }
    
    const session = await findSession({ userId: payload.id, refreshToken });
    
    if (session && session.expiration > new Date()) {
      const token = await setAuthTokens(payload.id, res, session);
      res.status(200).send({ token, user });
    } else {
      res.status(401).send('Refresh token expired');
    }
  } catch (err) {
    logger.error('[refreshController] Invalid refresh token:', err);
    res.status(403).send('Invalid refresh token');
  }
};

User Controllers

Get User Controller

File: api/server/controllers/UserController.js
const { getAppConfig } = require('~/server/services/Config');
const { needsRefresh, getNewS3URL } = require('~/server/services/Files/S3/crud');

const getUserController = async (req, res) => {
  const appConfig = await getAppConfig({ role: req.user?.role });
  const userData = req.user.toObject();
  
  // Remove sensitive fields
  delete userData.password;
  delete userData.totpSecret;
  delete userData.backupCodes;
  
  // Handle S3 avatar refresh
  if (appConfig.fileStrategy === 's3' && userData.avatar) {
    const avatarNeedsRefresh = needsRefresh(userData.avatar, 3600);
    if (avatarNeedsRefresh) {
      try {
        userData.avatar = await getNewS3URL(userData.avatar);
        await updateUser(userData.id, { avatar: userData.avatar });
      } catch (error) {
        logger.error('Error refreshing avatar URL:', error);
      }
    }
  }
  
  res.status(200).send(userData);
};

Update User Plugins Controller

const { updateUserPlugins } = require('~/models');
const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService');

const updateUserPluginsController = async (req, res) => {
  const { user } = req;
  const { pluginKey, action, auth } = req.body;
  
  try {
    // Update plugin installation status
    await updateUserPlugins(user._id, user.plugins, pluginKey, action);
    
    if (!auth) {
      return res.status(200).send();
    }
    
    const keys = Object.keys(auth);
    const values = Object.values(auth);
    
    if (action === 'install') {
      // Store plugin credentials
      for (let i = 0; i < keys.length; i++) {
        await updateUserPluginAuth(user.id, keys[i], pluginKey, values[i]);
      }
    } else if (action === 'uninstall') {
      // Remove plugin credentials
      for (let i = 0; i < keys.length; i++) {
        await deleteUserPluginAuth(user.id, keys[i]);
      }
    }
    
    return res.status(200).send();
  } catch (err) {
    logger.error('[updateUserPluginsController]', err);
    return res.status(500).json({ message: 'Something went wrong.' });
  }
};

Delete User Controller

Handles complete user account deletion:
const {
  deleteMessages,
  deletePresets,
  deleteConvos,
  deleteFiles,
} = require('~/models');
const { Transaction, Balance, Assistant } = require('~/db/models');

const deleteUserController = async (req, res) => {
  const { user } = req;
  
  try {
    await deleteMessages({ user: user.id });
    await Transaction.deleteMany({ user: user.id });
    await Balance.deleteMany({ user: user._id });
    await deletePresets(user.id);
    await deleteConvos(user.id);
    await deleteFiles(null, user.id);
    await Assistant.deleteMany({ user: user.id });
    await deleteUserById(user.id);
    
    logger.info(`User deleted account. Email: ${user.email} ID: ${user.id}`);
    res.status(200).send({ message: 'User deleted' });
  } catch (err) {
    logger.error('[deleteUserController]', err);
    return res.status(500).json({ message: 'Something went wrong.' });
  }
};

Model Controllers

Model Configuration Controller

File: api/server/controllers/ModelController.js
const { CacheKeys } = require('librechat-data-provider');
const { loadDefaultModels, loadConfigModels } = require('~/server/services/Config');
const { getLogStores } = require('~/cache');

const getModelsConfig = async (req) => {
  const cache = getLogStores(CacheKeys.CONFIG_STORE);
  let modelsConfig = await cache.get(CacheKeys.MODELS_CONFIG);
  
  if (!modelsConfig) {
    modelsConfig = await loadModels(req);
  }
  
  return modelsConfig;
};

async function loadModels(req) {
  const cache = getLogStores(CacheKeys.CONFIG_STORE);
  const cachedModelsConfig = await cache.get(CacheKeys.MODELS_CONFIG);
  
  if (cachedModelsConfig) {
    return cachedModelsConfig;
  }
  
  const defaultModelsConfig = await loadDefaultModels(req);
  const customModelsConfig = await loadConfigModels(req);
  const modelConfig = { ...defaultModelsConfig, ...customModelsConfig };
  
  await cache.set(CacheKeys.MODELS_CONFIG, modelConfig);
  return modelConfig;
}

async function modelController(req, res) {
  try {
    const modelConfig = await loadModels(req);
    res.send(modelConfig);
  } catch (error) {
    logger.error('Error fetching models:', error);
    res.status(500).send({ error: error.message });
  }
}

module.exports = { modelController, loadModels, getModelsConfig };

Controller Best Practices

1. Error Handling Pattern

const controllerFunction = async (req, res) => {
  try {
    // Business logic
    const result = await someOperation();
    res.status(200).json(result);
  } catch (error) {
    logger.error('[controllerFunction]', error);
    res.status(500).json({ message: 'Error message' });
  }
};

2. Input Validation

const updateController = async (req, res) => {
  const { id, name } = req.body;
  
  if (!id || !name) {
    return res.status(400).json({ error: 'Missing required fields' });
  }
  
  try {
    const result = await updateItem(id, { name });
    res.status(200).json(result);
  } catch (error) {
    logger.error('[updateController]', error);
    res.status(500).json({ error: 'Update failed' });
  }
};

3. Service Layer Delegation

// Good: Delegate to service
const controller = async (req, res) => {
  try {
    const result = await authService.register(req.body);
    res.status(200).json(result);
  } catch (error) {
    handleError(error, res);
  }
};

// Avoid: Business logic in controller
const controller = async (req, res) => {
  // Don't put complex business logic here
  const hash = bcrypt.hashSync(password, 10);
  const user = await createUser({ ...data, password: hash });
  // ...
};

4. Consistent Response Format

const standardResponse = (res, status, data, message) => {
  return res.status(status).json({
    success: status < 400,
    data,
    message,
  });
};

const controller = async (req, res) => {
  try {
    const result = await operation();
    return standardResponse(res, 200, result, 'Success');
  } catch (error) {
    return standardResponse(res, 500, null, 'Error');
  }
};

5. Logging

const controller = async (req, res) => {
  try {
    logger.debug(`[controller] Processing request for user: ${req.user.id}`);
    const result = await operation();
    logger.info(`[controller] Operation successful for user: ${req.user.id}`);
    res.status(200).json(result);
  } catch (error) {
    logger.error('[controller] Operation failed:', error);
    res.status(500).json({ error: 'Operation failed' });
  }
};

Testing Controllers

const { getUserController } = require('~/server/controllers/UserController');

describe('UserController', () => {
  it('should return user data', async () => {
    const req = {
      user: { id: '123', email: '[email protected]', toObject: () => ({}) },
    };
    const res = {
      status: jest.fn().mockReturnThis(),
      send: jest.fn(),
    };
    
    await getUserController(req, res);
    
    expect(res.status).toHaveBeenCalledWith(200);
    expect(res.send).toHaveBeenCalled();
  });
});

Build docs developers (and LLMs) love