DEV Community

Cover image for 5 Wix Velo mistakes I fix on almost every client site
Mosaddik Billah
Mosaddik Billah

Posted on Edited on

5 Wix Velo mistakes I fix on almost every client site

I'm a Wix developer, and a lot of my work starts with a site someone else built. The Velo code changes from project to project, but the same five problems keep coming up. None of them are hard to fix once you know what to look for.

1. The API key is in page code

This is the most common one, and the most serious:

// Page code: runs in the visitor's browser
const response = await fetch("https://api.example.com/forecast?city=Dhaka", {
  headers: { Authorization: "Bearer sk_live_abc123..." },
});
Enter fullscreen mode Exit fullscreen mode

Page code runs in the browser, so anyone can open the developer tools, look at the network request and copy the key.

The fix: store the key in the Secrets Manager (Dashboard → Developer Tools → Secrets Manager) and make the request from a backend web module:

// backend/weather.web.js: runs on the server
import { Permissions, webMethod } from "wix-web-module";
import { getSecret } from "wix-secrets-backend";
import { fetch } from "wix-fetch";

export const getForecast = webMethod(Permissions.Anyone, async (city) => {
  const apiKey = await getSecret("WEATHER_API_KEY");
  const url = `https://api.example.com/forecast?city=${encodeURIComponent(city)}`;
  const response = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });

  if (!response.ok) throw new Error(`Forecast request failed: ${response.status}`);

  const data = await response.json();
  return { summary: data.summary, temperature: data.temperature }; // only what the page needs
});
Enter fullscreen mode Exit fullscreen mode

Page code imports getForecast from backend/weather.web and calls it like any async function. The browser only ever talks to your own backend.

Full walkthrough: How to call an external API from Wix Velo without exposing your API key.

2. Every repeater item shows the same text

The repeater fills up, but every card shows the same title. The cause is almost always $w inside onItemReady:

// Wrong: $w targets the first matching element on the page
$w("#repeater").onItemReady(($item, itemData) => {
  $w("#title").text = itemData.title;
});
Enter fullscreen mode Exit fullscreen mode

The fix: use $item, which targets the elements inside this item:

$w("#repeater").onItemReady(($item, itemData) => {
  $item("#title").text = itemData.title;
});
Enter fullscreen mode Exit fullscreen mode

Two related problems: if nothing shows at all, check that every item has a unique string _id. And if the repeater is also connected to a dataset in the editor, the dataset and your code will fight over it, so use one or the other.

More repeater patterns: Wix Velo repeaters explained.

3. Form validation that only runs in the browser

Checking inputs in page code is good for the visitor: they get a clear message straight away. But page code can be skipped, so it can't be the only check for anything that matters.

The fix: repeat the important checks in a data hook, which runs on the server before the item is saved. In backend/data.js:

export function Bookings_beforeInsert(item, context) {
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  if (!item.date || new Date(item.date) < today) {
    return Promise.reject("The booking date must be today or later.");
  }
  item.email = item.email.trim().toLowerCase();
  return item;
}
Enter fullscreen mode Exit fullscreen mode

Rejecting the promise stops the insert. It's also the right place to clean up data, like lowercasing emails.

How to do both sides properly: How to validate a Wix form with Velo.

4. "Members only" data that isn't

A members' page shows each member their own orders using a filter in page code:

wixData.query("Requests").eq("_owner", member._id).find();
Enter fullscreen mode Exit fullscreen mode

That's correct, but on its own it doesn't protect anything. If the collection's permissions let anyone read every item, the data is still available to anyone who asks for it in a different way.

The fix: set the collection's Permissions so the database itself enforces it:

  • Read: Site member author
  • Create: Site member
  • Update: Site member author
  • Delete: Admin

Now the query above is a convenience, and the permissions are the real protection.

Step by step: How to show each Wix member only their own data.

5. Queries that wait for each other

This pattern makes pages noticeably slower:

$w.onReady(async () => {
  const products = await wixData.query("Products").find();
  const reviews = await wixData.query("Reviews").find();
  // ...fill the page
});
Enter fullscreen mode Exit fullscreen mode

Each await waits for the previous query to finish, even though they don't depend on each other. And each query loads the whole collection.

The fix: run independent queries in parallel and load only what you show:

$w.onReady(async () => {
  const [products, reviews] = await Promise.all([
    wixData.query("Products").limit(12).find(),
    wixData.query("Reviews").limit(5).find(),
  ]);
  // ...fill the page
});
Enter fullscreen mode Exit fullscreen mode

Then load content for tabs, popups and lower sections only when the visitor opens or scrolls to them.

Other causes of slow Wix sites, from images to apps: Why your Wix site is slow, and how to fix it.

A quick checklist

  • No API keys or secrets in page code
  • $item, not $w, inside onItemReady
  • Important checks repeated in a backend data hook
  • Collection permissions set, not just page-code filters
  • Independent queries in Promise.all, with .limit()

If you're learning Velo, I've put my guides in a reading order here: Learn Wix Velo, step by step.

What's the most common Velo problem you run into? I'd like to hear it in the comments.

Top comments (0)