Development of Custom Velo (Wix Code) Solutions
Velo by Wix is a JavaScript platform on top of Wix that provides access to full-scale programming: working with data through collection APIs, serverless functions (HTTP Functions, Web Methods), routing, OAuth, external APIs. This is not "some code" in a constructor — it's a development environment with its own patterns, limitations, and capabilities unavailable through the visual editor.
Velo Architecture
Velo is divided into two contexts:
Client-side code (pages and master page):
- Page files:
pages/myPage.js - Master page:
masterPage.js - Public modules:
public/utils.js(reusable code) - DOM access through
$w()API (jQuery-like selectors)
Backend code (serverless, Node.js):
- Web Methods:
backend/myService.web.js— called from client as functions - HTTP Functions:
backend/http-functions.js— REST API endpoints - Jobs:
backend/jobs.config.json— scheduled tasks (cron) - Collection hooks:
backend/data.js— beforeInsert, afterQuery, etc.
src/
├── pages/
│ ├── home.js
│ └── catalog.js
├── masterPage.js
├── public/
│ ├── utils.js
│ └── constants.js
└── backend/
├── http-functions.js
├── myService.web.js
├── data.js # collection hooks
└── jobs.config.json
HTTP Functions — External API
HTTP Functions allow creating public REST endpoints on the site domain: https://domain.com/_functions/myEndpoint.
// backend/http-functions.js
import { ok, badRequest, serverError } from 'wix-http-functions';
import wixData from 'wix-data';
export async function get_products(request) {
try {
const { query } = request;
const category = query.category;
let dbQuery = wixData.query('Products')
.eq('isActive', true);
if (category) {
dbQuery = dbQuery.eq('category', category);
}
const result = await dbQuery
.ascending('order')
.find({ suppressAuth: true });
return ok({
body: JSON.stringify({
items: result.items,
total: result.totalCount,
}),
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=300',
}
});
} catch (error) {
return serverError({ body: JSON.stringify({ error: error.message }) });
}
}
// POST endpoint
export async function post_leads(request) {
const body = await request.body.json();
// Validation
if (!body.email || !body.name) {
return badRequest({ body: JSON.stringify({ error: 'Missing required fields' }) });
}
// Honeypot
if (body.website) {
return ok({ body: JSON.stringify({ success: true }) }); // silently ignore bot
}
await wixData.insert('Leads', {
name: body.name,
email: body.email,
message: body.message,
source: body.source ?? 'direct',
createdAt: new Date(),
});
// Send to CRM via HTTP
await fetch(process.env.CRM_WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return ok({ body: JSON.stringify({ success: true }) });
}
Web Methods — Type-Safe Backend Calls
Web Methods are the preferred way to call server code from client within Velo:
// backend/catalogService.web.js
import { Permissions, webMethod } from 'wix-web-module';
import wixData from 'wix-data';
export const getFilteredProducts = webMethod(
Permissions.Anyone,
async ({ category, minPrice, maxPrice, page = 1, limit = 12 }) => {
let query = wixData.query('Products')
.eq('isActive', true)
.ge('price', minPrice ?? 0);
if (maxPrice) query = query.le('price', maxPrice);
if (category) query = query.eq('category', category);
const result = await query
.ascending('order')
.skip((page - 1) * limit)
.limit(limit)
.find();
return {
items: result.items,
total: result.totalCount,
pages: Math.ceil(result.totalCount / limit),
};
}
);
// pages/catalog.js
import { getFilteredProducts } from 'backend/catalogService.web';
$w.onReady(function () {
loadProducts({ category: null, page: 1 });
});
async function loadProducts(filters) {
$w('#loadingSpinner').show();
try {
const { items, total, pages } = await getFilteredProducts(filters);
$w('#repeater').data = items;
$w('#totalCount').text = `Found: ${total}`;
updatePagination(pages, filters.page);
} finally {
$w('#loadingSpinner').hide();
}
}
Collection Hooks — Business Logic on Data Operations
// backend/data.js
import { triggered } from 'wix-data';
// Auto-generate slug before insert
export function beforeInsert_Products(item, context) {
if (!item.slug) {
item.slug = item.title
.toLowerCase()
.replace(/[^а-яёa-z0-9]/gi, '-')
.replace(/-+/g, '-');
}
item.createdAt = new Date();
return item;
}
// Log changes
export async function afterUpdate_Products(item, context) {
await wixData.insert('AuditLog', {
entityType: 'Products',
entityId: item._id,
action: 'update',
userId: context.userId,
timestamp: new Date(),
});
return item;
}
// Cascade delete related records
export async function beforeRemove_Categories(item, context) {
const products = await wixData.query('Products')
.eq('category', item._id)
.find({ suppressAuth: true });
if (products.totalCount > 0) {
throw new Error(`Cannot delete category with ${products.totalCount} products`);
}
return item;
}
Routing and Custom URLs
// routers.js — custom router for /catalog/*
export async function catalog_Router(request) {
const slug = request.path[0];
if (!slug) {
return WixRouterSitemapEntry('/catalog');
}
const result = await wixData.query('Products')
.eq('slug', slug)
.eq('isActive', true)
.find({ suppressAuth: true });
if (result.items.length === 0) {
return notFound();
}
return ok('catalog-product', {
product: result.items[0],
relatedItems: [], // additional data for page
});
}
Integration with External APIs
External API calls — only from backend code (secrets are protected):
// backend/integrations.web.js
import { webMethod, Permissions } from 'wix-web-module';
import { getSecret } from 'wix-secrets-backend';
export const sendToTelegram = webMethod(
Permissions.Anyone,
async (message) => {
const botToken = await getSecret('TELEGRAM_BOT_TOKEN');
const chatId = await getSecret('TELEGRAM_CHAT_ID');
await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
text: message,
parse_mode: 'HTML',
})
});
}
);
Secrets are stored through Wix Secrets Manager — not in code.
Scheduled Jobs (Cron)
// backend/jobs.config.json
{
"jobs": [
{
"functionLocation": "/cleanupService",
"functionName": "cleanupOldLeads",
"executionConfig": {
"cronExpression": "0 3 * * *"
}
}
]
}
// backend/cleanupService.js
import wixData from 'wix-data';
export async function cleanupOldLeads() {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const oldLeads = await wixData.query('Leads')
.lt('createdAt', thirtyDaysAgo)
.eq('status', 'processed')
.find({ suppressAuth: true });
for (const lead of oldLeads.items) {
await wixData.remove('Leads', lead._id, { suppressAuth: true });
}
}
Typical Timelines
Custom form with validation, backend processing, and Telegram/CRM integration — 2-3 working days. Full-featured catalog with filtering, pagination, dynamic pages, and API — 7-12 days. Personal account with registration, profile, order history — 2-3 weeks.







