Architectural guidelines and best practices for developing upgrade-safe custom modules in EspoCRM. Covers backend PHP development (Controllers, Services, Hooks, ORM), frontend JavaScript (Backbone.js views), and JSON metadata configuration based on official documentation.
Scanned 6/17/2026
Install via CLI
openskills install aehenao/espocrm-module-development-best-practices---
name: espocrm-module-development-best-practices
description: Architectural guidelines and best practices for developing upgrade-safe custom modules in EspoCRM. Covers backend PHP development (Controllers, Services, Hooks, ORM), frontend JavaScript (Backbone.js views), and JSON metadata configuration based on official documentation.
when_to_use: Use this skill whenever the user requests to create, modify, review, or debug EspoCRM custom modules, entities, REST API endpoints, business logic hooks, or custom frontend views. It should also be strictly applied when designing database schemas via entityDefs metadata or interacting with the database using the EspoCRM ORM.
---
# EspoCRM Module Development Best Practices
## 1. AI Meta-Instruction
When requested to develop, modify, or review code for EspoCRM, you must strictly follow the architectural patterns and rules below, based on the official EspoCRM documentation. Always prioritize upgrade-safe compatibility and separation of concerns.
## 2. Golden Rules (Upgrade-Safe)
- **NEVER** modify EspoCRM core files (`application/Espo/`).
- **ALWAYS** place your code inside a custom module at `custom/Espo/Modules/{ModuleName}/`.
- Use the Hook system instead of modifying core controllers or services.
- Use Dependency Injection (DI) through the EspoCRM service container.
## 3. Module Directory Structure
Every module must follow this standard structure:
```text
custom/Espo/Modules/{ModuleName}/
├── Controllers/ # API Controllers (REST)
├── Entities/ # Entity Classes (Database Models)
├── Hooks/ # Business logic triggered by events (beforeSave, afterSave, etc.)
├── Resources/
│ ├── metadata/
│ │ ├── entityDefs/ # Field and relationship definitions (JSON)
│ │ ├── scopes/ # Module/entity configuration (JSON)
│ │ └── clientDefs/ # Frontend configuration (JSON)
│ └── layouts/ # View layouts (detail, list, relationships)
├── Services/ # Reusable business logic
├── SelectManagers/ # Complex filters and query logic
└── Hooks/ # Business logic hooks
```
## 4. Backend (PHP)
### 4.1. Namespaces
The namespace for any PHP class in a module must be:
```php
namespace Espo\Modules\{ModuleName}\{Folder};
```
### 4.2. Controllers
Controllers handle API requests. They must extend `Espo\Core\Controllers\Base` and use dependency injection. Return pure arrays (automatically converted to JSON).
```php
<?php
namespace Espo\Modules\MyModule\Controllers;
use Espo\Core\Controllers\Base;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
class MyEntity extends Base
{
public function getActionCustom(Request $request, Response $response): array
{
$id = $request->getQueryParam('id');
// Use injected service
$service = $this->getServiceFactory()->create('MyEntity');
return $service->getCustomData($id);
}
}
```
### 4.3. Services
Heavy business logic belongs in services. They extend `Espo\Core\Services\Base`.
```php
<?php
namespace Espo\Modules\MyModule\Services;
use Espo\Core\Services\Base;
use Espo\ORM\EntityManager;
class MyEntity extends Base
{
// Inject dependencies via constructor or typed properties
public function __construct(private EntityManager $entityManager) {}
public function getCustomData(string $id): array
{
$entity = $this->entityManager->getEntityById('MyEntity', $id);
if (!$entity) {
throw new \Espo\Core\Exceptions\NotFound();
}
return $entity->getValueMap();
}
}
```
### 4.4. Hooks (Business Logic)
Avoid modifying the `save` method directly. Use Hooks in `custom/Espo/Modules/MyModule/Hooks/MyEntity/MyHook.php`.
```php
<?php
namespace Espo\Modules\MyModule\Hooks\MyEntity;
use Espo\ORM\Entity;
use Espo\ORM\Repository\Hook\BeforeSaveHook;
class MyCustomHook implements BeforeSaveHook
{
public function beforeSave(Entity $entity, array $options = []): void
{
if ($entity->isAttributeChanged('status')) {
$entity->set('processedAt', date('Y-m-d H:i:s'));
}
}
}
```
### 4.5. Database & ORM
NEVER use raw SQL. Use the `EntityManager`.
- **Get entity:** `$em->getEntityById('Account', $id)`
- **Create entity:** `$em->createEntity('Account', ['name' => 'Test'])`
- **Queries (Find):**
```php
$builder = $this->entityManager->getQueryBuilder()
->select()
->from('Account')
->where(['type' => 'Customer'])
->limit(0, 10);
$collection = $this->entityManager->getCollection($builder);
```
## 5. Frontend (JavaScript / Backbone.js)
EspoCRM uses an enhanced Backbone.js architecture. Custom code goes in `client/custom/src/`.
### 5.1. Custom Views
To modify the behavior of a detail or list view, define the view in `clientDefs` and create the JS file.
```javascript
// client/custom/src/views/my-entity/detail.js
define('custom:views/my-entity/detail', ['views/detail'], function (Dep) {
return Dep.extend({
setup: function () {
Dep.prototype.setup.call(this);
// Custom logic after setup
this.listenTo(this.model, 'change:status', this.handleStatusChange);
},
handleStatusChange: function () {
// Frontend action
}
});
});
```
### 5.2. Frontend Metadata (`clientDefs.json`)
In `custom/Espo/Modules/MyModule/Resources/metadata/clientDefs/MyEntity.json`:
```json
{
"controller": "controllers/record",
"views": {
"list": "custom:views/my-entity/list",
"detail": "custom:views/my-entity/detail"
}
}
```
## 6. Metadata & Schema (`entityDefs`)
Database schema definition is done via JSON, not manual SQL migrations (for standard fields).
Path: `custom/Espo/Modules/MyModule/Resources/metadata/entityDefs/MyEntity.json`
```json
{
"fields": {
"name": {
"type": "varchar",
"required": true,
"maxLength": 255
},
"status": {
"type": "enum",
"options": ["Active", "Inactive", "Pending"],
"default": "Pending"
}
},
"links": {
"accounts": {
"type": "hasMany",
"foreign": "myEntities",
"entity": "Account",
"relationName": "accountMyEntity"
}
}
}
```
*Note: After changing `entityDefs`, the administrator must go to Administration -> Rebuild for EspoCRM to update the database schema and clear the cache.*
## 7. Code Standards & Security
1. **Strict Typing:** Always use `declare(strict_types=1);` at the beginning of PHP files.
2. **Return Types:** Always define return types in controller and service functions (`: array`, `: void`, `: Entity`).
3. **Security (ACL):** Always check user permissions in controllers before returning data.
```php
if (!$this->getUser()->isAllowed('MyEntity', 'read')) {
throw new Forbidden();
}
```
4. **Exception Handling:** Throw native EspoCRM exceptions (`Espo\Core\Exceptions\BadRequest`, `NotFound`, `Forbidden`) instead of returning error strings.
## 8. Suggested Development Process
1. Define the schema in `entityDefs`.
2. Run "Rebuild" in the system (or via CLI `php rebuild.php`).
3. Create the `Services` for business logic.
4. Create the `Controllers` to expose the API.
5. Adjust the `layouts` for the UI.
6. Only modify JS views (`clientDefs`) if the default behavior is insufficient.No comments yet. Be the first to comment!