Route Structure and Patterns
LibreChat uses Express.js for routing with a modular, feature-based organization. Routes handle incoming HTTP requests and delegate business logic to controllers and services.Route Organization
Routes are organized by feature inapi/server/routes/:
api/server/routes/
├── index.js # Central route registry
├── auth.js # Authentication routes
├── user.js # User management
├── convos.js # Conversations
├── messages.js # Messages
├── prompts.js # Prompt management
├── assistants.js # AI assistants
├── agents.js # AI agents
├── files.js # File operations
├── models.js # Model configuration
├── endpoints.js # Endpoint configuration
└── ...
Central Route Registry
All routes are exported fromapi/server/routes/index.js:
const accessPermissions = require('./accessPermissions');
const assistants = require('./assistants');
const categories = require('./categories');
const endpoints = require('./endpoints');
const messages = require('./messages');
const prompts = require('./prompts');
const convos = require('./convos');
const agents = require('./agents');
const auth = require('./auth');
const user = require('./user');
module.exports = {
auth,
user,
convos,
prompts,
messages,
endpoints,
assistants,
agents,
accessPermissions,
// ...
};
Route Patterns
Basic Route Structure
Routes follow a consistent pattern:const express = require('express');
const { requireJwtAuth } = require('~/server/middleware');
const { getUserController } = require('~/server/controllers/UserController');
const router = express.Router();
// Apply middleware
router.use(requireJwtAuth);
// Define routes
router.get('/', getUserController);
module.exports = router;
Authentication Routes
File:api/server/routes/auth.js
const express = require('express');
const {
resetPasswordRequestController,
resetPasswordController,
registrationController,
refreshController,
} = require('~/server/controllers/AuthController');
const middleware = require('~/server/middleware');
const router = express.Router();
// Login with rate limiting and ban checking
router.post(
'/login',
middleware.loginLimiter,
middleware.checkBan,
middleware.requireLocalAuth,
loginController,
);
// Registration with validation
router.post(
'/register',
middleware.registerLimiter,
middleware.checkBan,
middleware.validateRegistration,
registrationController,
);
// Password reset
router.post(
'/requestPasswordReset',
middleware.resetPasswordLimiter,
middleware.validatePasswordReset,
resetPasswordRequestController,
);
module.exports = router;
User Routes
File:api/server/routes/user.js
const express = require('express');
const {
updateUserPluginsController,
deleteUserController,
getUserController,
} = require('~/server/controllers/UserController');
const {
requireJwtAuth,
canDeleteAccount,
} = require('~/server/middleware');
const router = express.Router();
router.get('/', requireJwtAuth, getUserController);
router.post('/plugins', requireJwtAuth, updateUserPluginsController);
router.delete('/delete',
requireJwtAuth,
canDeleteAccount,
deleteUserController
);
module.exports = router;
Conversation Routes
File:api/server/routes/convos.js
const express = require('express');
const { getConvosByCursor, deleteConvos, getConvo } = require('~/models/Conversation');
const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
const router = express.Router();
router.use(requireJwtAuth);
// Get conversations with cursor pagination
router.get('/', async (req, res) => {
const limit = parseInt(req.query.limit, 10) || 25;
const cursor = req.query.cursor;
const isArchived = req.query.isArchived === 'true';
const search = req.query.search ? decodeURIComponent(req.query.search) : undefined;
try {
const result = await getConvosByCursor(req.user.id, {
cursor,
limit,
isArchived,
search,
});
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: 'Error fetching conversations' });
}
});
// Get single conversation
router.get('/:conversationId', async (req, res) => {
const convo = await getConvo(req.user.id, req.params.conversationId);
if (convo) {
res.status(200).json(convo);
} else {
res.status(404).end();
}
});
// Delete conversations
router.delete('/', async (req, res) => {
const { conversationId } = req.body?.arg ?? {};
const filter = conversationId ? { conversationId } : {};
try {
const dbResponse = await deleteConvos(req.user.id, filter);
res.status(201).json(dbResponse);
} catch (error) {
res.status(500).send('Error deleting conversations');
}
});
module.exports = router;
Advanced Route Patterns
ACL-Protected Routes
Routes with Access Control Lists (ACL) for resource permissions: File:api/server/routes/prompts.js
const express = require('express');
const {
canAccessPromptGroupResource,
requireJwtAuth,
} = require('~/server/middleware');
const { PermissionBits } = require('librechat-data-provider');
const router = express.Router();
router.use(requireJwtAuth);
// Get prompt group with VIEW permission check
router.get(
'/groups/:groupId',
canAccessPromptGroupResource({
requiredPermission: PermissionBits.VIEW,
}),
async (req, res) => {
const group = await getPromptGroup({ _id: req.params.groupId });
res.status(200).send(group);
},
);
// Update prompt group with EDIT permission check
router.patch(
'/groups/:groupId',
canAccessPromptGroupResource({
requiredPermission: PermissionBits.EDIT,
}),
patchPromptGroup,
);
// Delete prompt group with DELETE permission check
router.delete(
'/groups/:groupId',
canAccessPromptGroupResource({
requiredPermission: PermissionBits.DELETE,
}),
deletePromptGroupController,
);
File Upload Routes
const multer = require('multer');
const { storage, importFileFilter } = require('~/server/routes/files/multer');
const upload = multer({ storage, fileFilter: importFileFilter });
router.post(
'/import',
importIpLimiter,
importUserLimiter,
upload.single('file'),
async (req, res) => {
await importConversations({
filepath: req.file.path,
requestUserId: req.user.id,
});
res.status(201).json({ message: 'Imported successfully' });
},
);
Nested Route Structure
For sub-resources:const settings = require('./settings');
const router = express.Router();
// Mount sub-router
router.use('/settings', settings);
// Creates routes like: /api/user/settings/*
Route Middleware Patterns
Common Middleware Stack
router.post(
'/route',
middleware.rateLimiter, // Rate limiting
middleware.checkBan, // Ban checking
middleware.requireJwtAuth, // Authentication
middleware.validateInput, // Input validation
middleware.checkPermissions, // Authorization
controllerFunction // Handler
);
Custom Middleware
const validateConvoAccess = async (req, res, next) => {
const { conversationId } = req.body?.arg ?? {};
const convo = await getConvo(req.user.id, conversationId);
if (!convo) {
return res.status(404).json({ error: 'Conversation not found' });
}
next();
};
router.post('/archive', validateConvoAccess, archiveController);
Query Parameters
Pagination with Cursor
router.get('/', async (req, res) => {
const limit = parseInt(req.query.limit, 10) || 25;
const cursor = req.query.cursor;
const result = await getConvosByCursor(req.user.id, { cursor, limit });
res.json(result);
});
Filtering and Search
router.get('/', async (req, res) => {
const { name, category, pageSize } = req.query;
const search = req.query.search ? decodeURIComponent(req.query.search) : undefined;
const filter = { name, category };
const result = await getPromptGroups(req, filter);
res.json(result);
});
Error Handling
Standard Error Response
router.get('/:id', async (req, res) => {
try {
const item = await getItem(req.params.id);
if (!item) {
return res.status(404).json({ error: 'Not found' });
}
res.status(200).json(item);
} catch (error) {
logger.error('Error getting item', error);
res.status(500).json({ error: 'Internal server error' });
}
});
Best Practices
1. Use Middleware for Cross-Cutting Concerns
// Apply authentication to all routes
router.use(requireJwtAuth);
// Then define specific routes
router.get('/', getController);
router.post('/', createController);
2. Validate Input Early
router.post('/update', validateConvoAccess, async (req, res) => {
const { conversationId, title } = req.body?.arg ?? {};
if (!conversationId || !title) {
return res.status(400).json({ error: 'Missing required fields' });
}
// Process update...
});
3. Keep Routes Thin
Delegate business logic to controllers:// Good: Route delegates to controller
router.post('/register', registrationController);
// Avoid: Business logic in route
router.post('/register', async (req, res) => {
// Don't put complex logic here
});
4. Use Consistent Response Formats
// Success
res.status(200).json({ data, message: 'Success' });
// Error
res.status(400).json({ error: 'Error message' });
// Not found
res.status(404).json({ error: 'Resource not found' });
Route Testing
Test routes using Jest:const request = require('supertest');
const app = require('~/app');
describe('User Routes', () => {
it('should get user data', async () => {
const response = await request(app)
.get('/api/user')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('email');
});
});
Related Documentation
- Controllers - Controller implementation
- Middleware - Authentication and authorization
- Models - Database models