Template Syntax Guide
Template Syntax Guide Enfyra provides three equivalent ways to access context properties. You can use the full $ctx.$property syntax, template syntax, or direct table syntax - all work exactly the same way. Overview All three syntaxes are fully supported and equivalent: // Full s
Template Syntax Guide
Enfyra provides three equivalent ways to access context properties. You can use the full $ctx.$property syntax, template syntax, or direct table syntax - all work exactly the same way.
Overview
All three syntaxes are fully supported and equivalent:
// Full syntax (always works)
const data = await $ctx.$cache.get('key');
const users = await $ctx.$repos.enfyra_user.find({...});
const slug = $ctx.$helpers.autoSlug('Hello World');
$ctx.$logs('Operation completed');
// Template syntax (convenience shortcut)
const data = await @CACHE.get('key');
const users = await @REPOS.enfyra_user.find({...});
const slug = @HELPERS.autoSlug('Hello World');
@LOGS('Operation completed');
const userId = @USER.id;
const bodyData = @BODY.name;
// Direct table syntax (shortest for database)
const data = await @CACHE.get('key');
const users = await #enfyra_user.find({...});
const slug = @HELPERS.autoSlug('Hello World');
@LOGS('Operation completed');
const userId = @USER.id;
const bodyData = @BODY.name;
Template syntax is just syntactic sugar - Enfyra Server replaces it with the full $ctx.$property syntax before compiled code is passed to the kernel executor. Use whichever style you prefer - you can even mix all three in the same file!
In find() options, pass predicates as filter. REST list endpoints also use the query parameter name filter.
Available Templates
| Template | Replacement | Description |
|---|---|---|
@CACHE |
$ctx.$cache |
Cache operations and distributed locking |
@REPOS |
$ctx.$repos |
Database repository access |
@HELPERS |
$ctx.$helpers |
Utility functions and helpers |
@FETCH |
$ctx.$helpers.$fetch |
SSRF-hardened HTTP client (shorthand for @HELPERS.$fetch) |
@LOGS |
$ctx.$logs |
Logging functions |
@BODY |
$ctx.$body |
Request body data |
@ENV |
$ctx.$env |
Sanitized environment variables exposed by the host runtime |
@DATA |
$ctx.$data |
Response data object |
@STATUS |
$ctx.$statusCode |
HTTP status code (200 on success, error code on failure — available in postHooks) |
@ERROR |
$ctx.$error |
Error context in postHooks ({ message, name, statusCode, details, timestamp } — undefined on success) |
@PARAMS |
$ctx.$params |
Route parameters |
@QUERY |
$ctx.$query |
Query parameters |
@USER |
$ctx.$user |
Current user information |
@REQ |
$ctx.$req |
Express request object |
@RES |
$ctx.$res |
Express response object (handlers only) |
@SHARE |
$ctx.$share |
Shared data between hooks |
@API |
$ctx.$api |
API request/response information |
@SOCKET |
$ctx.$socket |
WebSocket operations (join, leave, reply, emitToUser, emitToRoom, emitToCurrentRoom, broadcastToRoom, emitToGateway, broadcast; disconnect only in connection handlers) |
@TRIGGER |
$ctx.$trigger |
Trigger a flow by id or name (@TRIGGER(flowIdOrName, payload?)) |
@FLOW |
$ctx.$flow |
Current flow context inside flow steps (payload, last step output, meta) |
@FLOW_PAYLOAD |
$ctx.$flow.$payload |
Original payload passed into the flow |
@FLOW_LAST |
$ctx.$flow.$last |
Output of the previous flow step |
@FLOW_META |
$ctx.$flow.$meta |
Flow execution metadata (id, name, runId, etc.) |
@UPLOADED_FILE |
$ctx.$uploadedFile |
Uploaded file information |
@PKGS |
$ctx.$pkgs |
Installed npm packages for use in handlers |
@THROW |
$ctx.$throw |
Error throwing functions |
@THROW400 |
$ctx.$throw['400'] |
HTTP 400 Bad Request (shortcut) |
@THROW401 |
$ctx.$throw['401'] |
HTTP 401 Unauthorized (shortcut) |
@THROW403 |
$ctx.$throw['403'] |
HTTP 403 Forbidden (shortcut) |
@THROW404 |
$ctx.$throw['404'] |
HTTP 404 Not Found (shortcut) |
@THROW409 |
$ctx.$throw['409'] |
HTTP 409 Conflict (shortcut) |
@THROW422 |
$ctx.$throw['422'] |
HTTP 422 Validation Error (shortcut) |
@THROW429 |
$ctx.$throw['429'] |
HTTP 429 Rate Limit Exceeded (shortcut) |
@THROW500 |
$ctx.$throw['500'] |
HTTP 500 Internal Error (shortcut) |
@THROW503 |
$ctx.$throw['503'] |
HTTP 503 Service Unavailable (shortcut) |
#table_name |
$ctx.$repos.table_name |
Direct table access (e.g., #enfyra_user, #product) |
%pkg_name |
$ctx.$pkgs.pkg_name |
Shorthand package access (e.g., %axios, %lodash, %moment) |
Usage Examples
Cache Operations
@CACHE maps to the same managed user cache as $ctx.$cache. Use logical keys only; Enfyra applies the current app namespace internally. Do not include NODE_NAME, user_cache:, or Redis prefixes in template code. User-cache data is limited by REDIS_USER_CACHE_LIMIT_MB (default 30 MB), and Enfyra evicts least-recently-used user-cache keys when the allocation is exceeded.
// Get data from cache
const cachedData = await @CACHE.get('user:123');
// Set data in cache with TTL
await @CACHE.set('user:123', userData, 3600000); // 1 hour
// Check if key exists
const exists = await @CACHE.exists('user:123');
// Delete from cache
await @CACHE.deleteKey('user:123');
// Distributed locking
const lockAcquired = await @CACHE.acquire('critical-operation', 'instance-1', 30000);
if (lockAcquired) {
try {
// Critical operation here
await performCriticalOperation();
} finally {
await @CACHE.release('critical-operation', 'instance-1');
}
}
Database Operations
Using @REPOS syntax:
// Find records with filtering and pagination
const users = await @REPOS.enfyra_user.find({
filter: { isActive: true },
fields: 'id,email,name', // Only fetch required fields
limit: 10, // Max 10 records (default: 10)
sort: '-createdAt' // Sort by createdAt DESC
});
// Fetch ALL records (no limit)
const allUsers = await @REPOS.enfyra_user.find({
filter: { isActive: true },
limit: 0 // 0 = fetch all
});
// Multi-field sorting
const sorted = await @REPOS.enfyra_user.find({
sort: 'name,-createdAt' // Sort by name ASC, then createdAt DESC
});
// Nested relations (get related data in ONE query)
const usersWithPosts = await @REPOS.enfyra_user.find({
fields: 'id,email,posts.title,posts.createdAt', // Nested field: posts.title
filter: { isActive: true }
});
// Filter by nested relation
const usersInRole = await @REPOS.enfyra_user.find({
filter: {
role: {
name: { _eq: 'Admin' } // Filter by related role name
}
},
fields: 'id,email,role.name'
});
// Create new record
const newUser = await @REPOS.enfyra_user.create({
data: {
email: '[email protected]',
name: 'John Doe',
isActive: true
}
});
// Update record by ID
const updatedUser = await @REPOS.enfyra_user.update({
id: userId,
data: {
name: 'Jane Doe',
lastLogin: new Date()
}
});
// Delete record by ID
await @REPOS.enfyra_user.delete({ id: userId });
Using #table_name syntax (shorter):
// Find records with filtering and pagination
const users = await #enfyra_user.find({
filter: { isActive: true },
fields: 'id,email,name', // Only fetch required fields
limit: 10, // Max 10 records (default: 10)
sort: '-createdAt' // Sort by createdAt DESC
});
// Fetch ALL records (no limit)
const allUsers = await #enfyra_user.find({
filter: { isActive: true },
limit: 0 // 0 = fetch all
});
// Multi-field sorting
const sorted = await #enfyra_user.find({
sort: 'name,-createdAt' // Sort by name ASC, then createdAt DESC
});
// Nested relations (get related data in ONE query)
const usersWithPosts = await #enfyra_user.find({
fields: 'id,email,posts.title,posts.createdAt', // Nested field: posts.title
filter: { isActive: true }
});
// Filter by nested relation
const usersInRole = await #enfyra_user.find({
filter: {
role: {
name: { _eq: 'Admin' } // Filter by related role name
}
},
fields: 'id,email,role.name'
});
// Create new record
const newUser = await #enfyra_user.create({ data: {
email: '[email protected]',
name: 'John Doe',
isActive: true
}});
// Update record by ID
const updatedUser = await #enfyra_user.update({ id: userId, data: {
name: 'Jane Doe',
lastLogin: new Date()
}});
// Delete record by ID
await #enfyra_user.delete({ id: userId });
Helper Functions
// Generate JWT token (call $jwt as a function: payload, expiresIn)
const token = await @HELPERS.$jwt({ userId: 123, role: 'admin' }, '1h');
// Hash password (single argument; salt rounds are managed internally)
const hashedPassword = await @HELPERS.$bcrypt.hash('password123');
// Verify password
const isValid = await @HELPERS.$bcrypt.compare('password123', hashedPassword);
// Generate URL-friendly slug
const slug = @HELPERS.autoSlug('Hello World!'); // "hello-world"
Package Usage
Traditional Syntax:
// Access installed npm packages
const axios = $ctx.$pkgs.axios;
const lodash = $ctx.$pkgs.lodash;
const moment = $ctx.$pkgs.moment;
// Use package normally
const response = await axios.get('https://api.example.com/data');
// Transform data with lodash
const grouped = lodash.groupBy(response.data, 'category');
// Format dates with moment
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
Template Syntax (Shortened):
// Access installed npm packages
const axios = @PKGS.axios;
const lodash = @PKGS.lodash;
const moment = @PKGS.moment;
// Use package normally
const response = await axios.get('https://api.example.com/data');
// Transform data with lodash
const grouped = lodash.groupBy(response.data, 'category');
// Format dates with moment
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
Shorthand Syntax (%):
// Direct package access - shortest possible
const axios = %axios;
const lodash = %lodash;
const moment = %moment;
const response = await axios.get('https://api.example.com/data');
const summary = lodash.groupBy(response.data, 'category');
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
Mixed Syntax:
// You can mix all three ways!
const axios = %axios; // Shorthand syntax
const lodash = @PKGS.lodash; // Template syntax
const moment = $ctx.$pkgs.moment; // Traditional syntax
const response = await axios.get('https://api.example.com/data');
const summary = lodash.groupBy(response.data, 'category');
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
Logging
// Basic logging
@LOGS('User operation started');
// Log with data
@LOGS('User created:', { id: 123, email: '[email protected]' });
// Multiple parameters
@LOGS('Cache operation', 'key:', 'user:123', 'result:', cachedData);
// Error logging
@LOGS('Error occurred:', error.message, error.stack);
File Upload & Streaming
// Access uploaded file
const file = @UPLOADED_FILE;
@LOGS('File uploaded:', file.originalname, file.mimetype, file.size);
// Save uploaded request file to storage and enfyra_file.
// This streams from the server temp file and does not buffer the full file.
const savedFile = await @STORAGE.$upload({
file: @UPLOADED_FILE,
description: @BODY.description
});
// Stream response (for large files or image processing)
const { Readable } = require('stream');
const sharp = @PKGS.sharp;
// Download and resize image
const response = await fetch(@QUERY.imageUrl);
const stream = Readable.fromWeb(response.body);
const transformer = sharp()
.resize(800, 600, { fit: 'inside' })
.jpeg({ quality: 85 });
// Stream to client (memory efficient!)
@RES.stream(stream.pipe(transformer), {
mimetype: 'image/jpeg',
filename: 'resized-image.jpg'
});
** See File Handling** for complete guide on file uploads, streaming, and image processing.
Error Handling
HTTP Status Code Errors (@THROW):
Option 1: With brackets and quotes
// Throw HTTP 400 Bad Request
@THROW['400']('Email is required');
// Throw HTTP 401 Unauthorized
@THROW['401']('Invalid credentials');
// Throw HTTP 403 Forbidden
@THROW['403']('Insufficient permissions');
// Throw HTTP 404 Not Found
@THROW['404']('User not found', 'user_id_123');
// Throw HTTP 409 Conflict (for duplicates)
@THROW['409']('Email already exists', 'email', '[email protected]');
// Throw HTTP 422 Validation Error
@THROW['422']('Invalid data format');
// Throw HTTP 500 Internal Server Error
@THROW['500']('Database connection failed');
Option 2: Direct shortcuts (no quotes needed)
// Throw HTTP 400 Bad Request
@THROW400('Email is required');
// Throw HTTP 401 Unauthorized
@THROW401('Invalid credentials');
// Throw HTTP 403 Forbidden
@THROW403('Insufficient permissions');
// Throw HTTP 404 Not Found
@THROW404('User not found', 'user_id_123');
// Throw HTTP 409 Conflict (for duplicates)
@THROW409('Email already exists');
// Throw HTTP 422 Validation Error
@THROW422('Invalid data format');
// Throw HTTP 429 Rate Limit Exceeded
@THROW429(100, 'per minute');
// Throw HTTP 500 Internal Server Error
@THROW500('Database connection failed');
// Throw HTTP 503 Service Unavailable
@THROW503('Service unavailable');
Advanced Usage
Chaining Operations
// Cache with database fallback
let data = await @CACHE.get('products:featured');
if (!data) {
data = await #products.find({
filter: { featured: true },
fields: 'id,name,price,image'
});
await @CACHE.set('products:featured', data, 3600000);
}
@LOGS('Featured products loaded:', data.length);
Error Handling with Logging
try {
const user = await #enfyra_user.find({ filter: { id: userId } });
if (!user.data.length) {
@THROW404('User not found');
}
const updatedUser = await #enfyra_user.update({
id: userId,
data: { lastLogin: new Date() }
});
@LOGS('User login updated:', updatedUser.id);
return updatedUser;
} catch (error) {
@LOGS('User update failed:', error.message);
throw error;
}
Complex Business Logic
// User registration with validation and caching
async function registerUser(userData) {
// Check if user exists
const existingUser = await #enfyra_user.find({
filter: { email: userData.email },
fields: 'id'
});
if (existingUser.data.length > 0) {
@THROW409('Email already exists');
}
// Hash password
const hashedPassword = await @HELPERS.$bcrypt.hash(userData.password);
// Create user
const newUser = await #enfyra_user.create({ data: {
...userData,
password: hashedPassword,
createdAt: new Date()
});
// Cache user data
await @CACHE.set(`user:${newUser.id}`, newUser, 1800000); // 30 minutes
// Generate JWT token
const token = await @HELPERS.$jwt({ userId: newUser.id }, '24h');
@LOGS('User registered successfully:', newUser.id);
return { user: newUser, token };
}
How Template Replacement Works
Template syntax is purely a convenience feature. Here's what happens behind the scenes:
- Code Submission: Your code with
@CACHE,@REPOS, etc. is submitted - Template Processing: Templates are automatically replaced with full
$ctx.$propertysyntax - Code Execution: The processed code runs normally in the handler executor
- Result Return: Normal execution continues with the replaced syntax
Example Transformation
// Your code (template syntax):
const data = await @CACHE.get('key');
const users = await #enfyra_user.find({...});
// What actually runs (after replacement):
const data = await $ctx.$cache.get('key');
const users = await $ctx.$repos.enfyra_user.find({...});
** The replacement is transparent** - you can mix all three syntaxes in the same file if you want!
Best Practices
1. Choose Your Style (All Work!)
// Option 1 - Full syntax throughout
const user = await $ctx.$repos.users.find({ filter: { id: userId } });
await $ctx.$cache.set(`user:${userId}`, user);
$ctx.$logs('User cached:', userId);
// Option 2 - Template syntax throughout
const user = await @REPOS.users.find({ filter: { id: userId } });
await @CACHE.set(`user:${userId}`, user);
@LOGS('User cached:', userId);
// Option 3 - Direct table syntax (shortest for database)
const user = await #enfyra_user.find({ filter: { id: userId } });
await @CACHE.set(`user:${userId}`, user);
@LOGS('User cached:', userId);
// Option 4 - Mix all three (totally fine!)
const user = await #enfyra_user.find({ filter: { id: userId } }); // Direct table
await $ctx.$cache.set(`user:${userId}`, user); // Full syntax
@LOGS('User cached:', userId); // Template syntax
// Option 5 - Any combination works!
const user = await $ctx.$repos.enfyra_user.find({ filter: { id: userId } }); // Full
await @CACHE.set(`user:${userId}`, user); // Template
const products = await #product.find({ filter: { userId } }); // Direct
$ctx.$logs('All operations completed'); // Full
2. Leverage Field Selection
// Good - only fetch needed fields
const users = await #enfyra_user.find({
filter: { isActive: true },
fields: 'id,email,name' // Performance optimization
});
// Good - fetch editable script source without generated compiled output
const handlers = await #enfyra_route_handler.find({
fields: '-compiledCode'
});
// Avoid fetching all fields unnecessarily
const users = await #enfyra_user.find({
filter: { isActive: true }
// Fetches all fields by default
});
When any field token starts with -, that fields scope uses exclude mode. For example, fields: 'id,-compiledCode' still returns all readable fields except compiledCode.
3. Proper Error Handling
// Good - comprehensive error handling
try {
const result = await @REPOS.users.create({ data: userData });
@LOGS('User created successfully');
return result;
} catch (error) {
@LOGS('User creation failed:', error.message);
@THROW500('Failed to create user');
}
4. Cache Strategy
// Good - cache with appropriate TTL
const cacheKey = `user:${userId}`;
let user = await @CACHE.get(cacheKey);
if (!user) {
user = await @REPOS.users.find({ filter: { id: userId } });
await @CACHE.set(cacheKey, user, 1800000); // 30 minutes
}
Compatibility & Flexibility
- ** All Three Syntaxes Supported**: Use
$ctx.$property,@TEMPLATE, OR#table_name- your choice! - ** Mix and Match**: You can use any combination in the same file
- ** All Contexts**: Works in Bootstrap Scripts, Hooks, and Custom Handlers
- ** IDE Support**:
$ctx.$propertyhas better autocomplete support - ** Debugging**: Stack traces show the processed
$ctx.$propertysyntax - ** No Performance Impact**: Template replacement is just string processing
- ** Maximum Flexibility**: Choose the syntax that fits your style best
Related Documentation
- Context Reference - Complete
$ctxobject documentation - File Handling - File upload and response streaming guide
- Custom Handlers - Using templates in handlers
- PreHooks - Using templates in prehooks
- postHooks - Using templates in postHooks