Files
MosisService/portal/internal/web/docs/guides/best-practices.md

9.5 KiB

Best Practices

Guidelines for building high-quality Mosis apps that users love.

Performance

Minimize DOM Queries

Cache element references instead of querying repeatedly:

-- Bad: Queries on every frame
function updateScore()
    document:GetElementById("score").inner_rml = tostring(score)
end

-- Good: Cache the reference
local scoreElement

function onLoad()
    scoreElement = document:GetElementById("score")
end

function updateScore()
    scoreElement.inner_rml = tostring(score)
end

Batch DOM Updates

Group multiple changes together:

-- Bad: Multiple separate updates
elem1.style.color = "red"
elem2.style.color = "red"
elem3.style.color = "red"

-- Good: Use a class
parent:SetClass("error-state", true)

Use Efficient Data Structures

-- For frequent lookups, use tables as maps
local itemLookup = {}
for i, item in ipairs(items) do
    itemLookup[item.id] = item
end

-- O(1) lookup instead of O(n) search
local item = itemLookup["item-123"]

Clean Up Timers

Always clear intervals when navigating away:

local updateInterval

function onScreenLoad()
    updateInterval = setInterval(function()
        updateData()
    end, 1000)
end

function onScreenUnload()
    if updateInterval then
        clearInterval(updateInterval)
        updateInterval = nil
    end
end

Lazy Load Content

Don't load everything at startup:

-- Load data when user scrolls to section
function onSectionVisible(sectionId)
    if not loadedSections[sectionId] then
        loadSectionData(sectionId)
        loadedSections[sectionId] = true
    end
end

User Experience

Provide Feedback

Show users that actions are happening:

function onSubmit()
    -- Show loading state immediately
    submitButton:SetClass("loading", true)
    submitButton:SetAttribute("disabled", "disabled")

    http.post(url, data, function(response)
        submitButton:SetClass("loading", false)
        submitButton:RemoveAttribute("disabled")

        if response.ok then
            showSuccess("Saved!")
        else
            showError("Failed to save")
        end
    end)
end

Handle Errors Gracefully

Never show raw error messages to users:

http.get(url, function(response)
    if response.ok then
        displayData(json.decode(response.body))
    else
        -- User-friendly message
        showMessage("Unable to load data. Please check your connection.")

        -- Log details for debugging
        console.error("API error:", response.status, response.body)
    end
end)

Make Touch Targets Large Enough

Minimum 48dp for touchable elements:

.button {
    min-width: 48dp;
    min-height: 48dp;
    padding: 12dp 24dp;
}

.list-item {
    min-height: 56dp;
    padding: 16dp;
}

Support Undo for Destructive Actions

local deletedItem = nil
local undoTimeout = nil

function deleteItem(itemId)
    deletedItem = items[itemId]
    items[itemId] = nil
    updateList()

    showUndoSnackbar("Item deleted", function()
        -- Undo callback
        items[itemId] = deletedItem
        deletedItem = nil
        updateList()
    end)

    -- Clear undo after 5 seconds
    undoTimeout = setTimeout(function()
        deletedItem = nil
        permanentlyDelete(itemId)
    end, 5000)
end

Remember User State

Restore position and selections when returning:

function onScreenUnload()
    storage.set("list_scroll_position", scrollContainer.scroll_top)
    storage.set("selected_tab", currentTab)
end

function onScreenLoad()
    local scrollPos = storage.get("list_scroll_position")
    if scrollPos then
        scrollContainer.scroll_top = scrollPos
    end

    local tab = storage.get("selected_tab")
    if tab then
        selectTab(tab)
    end
end

Code Quality

Use Local Variables

Local variables are faster and prevent global pollution:

-- Bad: Global
count = 0

-- Good: Local
local count = 0

-- Good: Module-level local
local Utils = {}
local cache = {}  -- Private to module

Handle Edge Cases

function divide(a, b)
    if b == 0 then
        console.warn("Division by zero")
        return 0
    end
    return a / b
end

function getUsername(user)
    if not user then
        return "Unknown"
    end
    return user.name or user.email or "Unknown"
end

Use Meaningful Names

-- Bad
local t = {}
local n = 0

-- Good
local userScores = {}
local attemptCount = 0

-- Bad
function p(x)
    return x * 100
end

-- Good
function toPercentage(decimal)
    return decimal * 100
end

Keep Functions Small

Each function should do one thing:

-- Bad: Does too much
function processUser(userId)
    local user = fetchUser(userId)
    validateUser(user)
    updateUserStats(user)
    sendWelcomeEmail(user)
    logActivity(user)
    return formatUserResponse(user)
end

-- Good: Composed of small functions
function processNewUser(userId)
    local user = fetchUser(userId)
    if not isValidUser(user) then
        return nil, "Invalid user"
    end
    initializeUserStats(user)
    queueWelcomeEmail(user)
    return user
end

Comment Why, Not What

-- Bad: Describes what (obvious from code)
-- Increment counter by 1
counter = counter + 1

-- Good: Explains why
-- Reset retry count after successful connection
-- to prevent unnecessary backoff on next attempt
retryCount = 0

Security

Validate All Input

function searchItems(query)
    -- Sanitize input
    if type(query) ~= "string" then
        return {}
    end

    query = query:sub(1, 100)  -- Limit length
    query = query:gsub("[^%w%s]", "")  -- Remove special chars

    return performSearch(query)
end

Don't Trust External Data

http.get(url, function(response)
    local success, data = pcall(function()
        return json.decode(response.body)
    end)

    if not success then
        console.error("Invalid JSON from API")
        return
    end

    -- Validate structure
    if type(data.items) ~= "table" then
        console.error("Missing items array")
        return
    end

    processItems(data.items)
end)

Never Store Secrets in Code

-- Bad: Hardcoded API key
local API_KEY = "sk-12345abcde"

-- Good: Use environment/config
local apiKey = config.get("api_key")

Sanitize Display Content

When displaying user-generated content, prevent injection:

function displayComment(text)
    -- Escape HTML entities
    text = text:gsub("&", "&")
    text = text:gsub("<", "&lt;")
    text = text:gsub(">", "&gt;")

    commentElement.inner_rml = text
end

Accessibility

Use Semantic Elements

<!-- Bad: Divs for everything -->
<div class="button" onclick="submit()">Submit</div>

<!-- Good: Proper elements -->
<button onclick="submit()">Submit</button>

<!-- Good: Headings create hierarchy -->
<h1>Settings</h1>
<h2>Account</h2>
<h2>Notifications</h2>

Provide Text Alternatives

<!-- Images should describe their purpose -->
<img src="icons/search.tga" alt="Search"/>

<!-- Icons with meaning need labels -->
<button aria-label="Close">
    <img src="icons/close.tga"/>
</button>

Ensure Color Contrast

Text should have at least 4.5:1 contrast ratio:

/* Good contrast */
.light-text {
    color: #ffffff;
    background-color: #1a1a2e;  /* Contrast: 12.6:1 */
}

/* Bad contrast */
.low-contrast {
    color: #888888;
    background-color: #666666;  /* Contrast: 1.3:1 */
}

Don't Rely on Color Alone

/* Bad: Only color indicates error */
.error {
    color: red;
}

/* Good: Icon + color + text */
.error {
    color: #ff4444;
}
.error::before {
    content: "⚠ ";
}

Testing

Test Error States

Don't just test the happy path:

-- Test these scenarios:
-- 1. Empty data
-- 2. Network failure
-- 3. Invalid input
-- 4. Timeouts
-- 5. Missing permissions

Test Navigation Flows

Ensure users can:

  • Navigate forward and back
  • Return to the home screen
  • Handle the back button at any screen

Test Edge Cases

  • Very long text/names
  • Empty lists
  • Maximum values
  • Rapid repeated actions
  • Interrupted operations

Use Debug Logging

local DEBUG = true

function debugLog(...)
    if DEBUG then
        print("[DEBUG]", ...)
    end
end

-- In production build, set DEBUG = false

Deployment

Use Meaningful Version Numbers

Follow semantic versioning:

  • MAJOR: Breaking changes
  • MINOR: New features, backward compatible
  • PATCH: Bug fixes
{
  "version": "2.1.3",
  "version_code": 15
}

Write Good Release Notes

Version 2.1.0

New Features:
- Added dark mode support
- New export to PDF feature

Improvements:
- Faster loading times
- Better error messages

Bug Fixes:
- Fixed crash when opening empty files
- Fixed date format on some devices

Test Before Submitting

  1. Run on the Designer
  2. Test all features manually
  3. Check on a real device if possible
  4. Verify all assets load correctly
  5. Test offline behavior

Summary Checklist

Before submitting your app:

  • All features work as expected
  • Error states are handled gracefully
  • Loading states shown during async operations
  • Touch targets are at least 48dp
  • Text is readable (contrast ratio ≥ 4.5:1)
  • No console errors in normal usage
  • Timers and intervals cleaned up properly
  • User data persists correctly
  • App works after fresh install
  • Version number and code are updated
  • Release notes are meaningful

See Also