Best Practices
Guidelines for building robust, maintainable Fynerisor applications.
Code Organization
File Structure
Organize your apps logically:
goto-apps/
├── index.risor # Main menu/landing page
├── tools/
│ ├── index.risor # Tools menu
│ ├── search.risor # Search tool
│ └── report.risor # Report generator
└── shared/
├── config.json # Shared configuration
└── logo.png # Shared resourcesScript Comments
Document your code:
// Chemical Search Tool
// Author: John Doe
// Last updated: 2026-03-15
//
// Searches the chemical database by CID, SMILES, or name
// and displays results in a paginated table.
// Initialize database connection
conn := sql.connect("sqlserver://server/db", {"stream": true})
// Configure search parameters
searchLimit := 100Performance
Efficient Data Loading
Load data on demand, not all at once:
// Good: Load data with pagination
table.Data(func(offset, limit) {
query := "SELECT * FROM data OFFSET " + string(offset) +
" ROWS FETCH NEXT " + string(limit) + " ROWS ONLY"
return conn.query(query)
})
// Bad: Load all data upfront
allData := conn.query("SELECT * FROM data") // Could be millions of rows!Lazy Initialization
Create expensive resources only when needed:
conn := nil // Start as nil
btnSearch := widget.NewButton("Search", func() {
if conn == nil {
window.SetStatus("Connecting to database...")
conn = sql.connect("sqlserver://server/db", {"stream": true})
}
// Perform search
})Caching Results
Cache API responses when appropriate:
cache := {}
func fetchData(id) {
if cache[id] != nil {
return cache[id]
}
data := fetch("https://api.example.com/data/" + id).json()
cache[id] = data
return data
}User Experience
Loading Indicators
Always provide feedback for long operations:
btnProcess := widget.NewButton("Process", func() {
window.SetStatus("Processing... Please wait")
// Long operation
result := processLargeFile(file)
window.SetStatus("Done! Processed " + string(result.count) + " items")
})Error Handling
Handle errors gracefully:
btnLoad := widget.NewButton("Load", func() {
if !os.exists(path.Text) {
window.SetStatus("Error: File not found")
return
}
data := os.read_file(path.Text)
if len(data) == 0 {
window.SetStatus("Warning: File is empty")
return
}
// Process data
window.SetStatus("File loaded successfully")
})Input Validation
Validate user input:
btnSubmit := widget.NewButton("Submit", func() {
// Trim whitespace
username := strings.trim_space(txtUsername.Text)
// Check required fields
if username == "" {
window.SetStatus("Error: Username is required")
return
}
// Validate format
if !strings.contains(username, "@") {
window.SetStatus("Error: Invalid email format")
return
}
// Submit
submit(username)
})Security
SQL Injection Prevention
Always escape user input for SQL:
func sqlEscape(text) {
return strings.replace_all(text, "'", "''")
}
query := "SELECT * FROM users WHERE name = '" + sqlEscape(userInput) + "'"Better: Use parameterized queries if available.
Path Traversal Prevention
Validate file paths:
func isValidPath(path) {
// Don't allow .. in paths
if strings.contains(path, "..") {
return false
}
// Ensure path is in allowed directory
if !strings.starts_with(path, "S:\\allowed\\") {
return false
}
return true
}
if !isValidPath(userPath) {
window.SetStatus("Error: Invalid path")
return
}Credential Protection
Never log or display credentials:
// Good
window.SetStatus("Connecting to " + server + "...")
// Bad - exposes password in status bar
window.SetStatus("Connecting as " + username + ":" + password + "...")Testing
Test with Sample Data
Create test data for development:
// Development mode
devMode := true
if devMode {
data := [
["Test1", "Value1"],
["Test2", "Value2"],
]
} else {
data = fetchRealData()
}Error Simulation
Test error handling:
// Simulate API failure
if devMode {
response := {"Success": false, "ErrorMsg": "Connection timeout"}
} else {
response = fetch("https://api.example.com/data").json()
}
if !response.Success {
window.SetStatus("Error: " + response.ErrorMsg)
return
}Maintainability
Reusable Functions
Extract common logic:
func createStandardTable(title, data, columns) {
table := widget.NewTable(title, 20)
table.Columns(func() { return columns })
table.RowCount(func() { return len(data) })
table.Data(func(offset, limit) {
if limit > len(data) {
limit = len(data)
}
return data[offset:limit]
})
table.Refresh()
return table
}
// Use it
myTable := createStandardTable("Users", userData, ["ID", "Name", "Email"])Configuration Files
Store configuration separately:
// Load config from JSON file
configText := os.read_file(gui.CurrentBaseURL + "config.json")
config := json.parse(string(configText))
apiUrl := config.apiUrl
pageSize := config.pageSizeVersion Information
Include version info in your apps:
version := "v1.2.0"
about := widget.NewLabel("Chemical Search Tool " + version + "\nLast updated: 2026-03-15")Naming Conventions
Use clear, descriptive names:
// Good
btnSubmitSearch := widget.NewButton("Search", searchHandler)
txtChemicalName := widget.NewEntry()
tblSearchResults := widget.NewTable("results", 20)
// Avoid
b1 := widget.NewButton("Go", fn)
e := widget.NewEntry()
t := widget.NewTable("x", 20)Common Patterns
Menu with Back Button
func createStandardLayout(title, content) {
header := widget.NewLabel(title)
btnBack := widget.NewButton("← Back", func() { gui.goto("..") })
top := container.NewBorder(nil, nil, header, btnBack, nil)
return container.NewBorder(top, nil, nil, nil, content)
}Form with Validation
func validateForm(fields) {
for _, field := range fields {
if field.Text == "" {
return "All fields are required"
}
}
return ""
}
btnSubmit := widget.NewButton("Submit", func() {
error := validateForm([name, email, phone])
if error != "" {
window.SetStatus("Error: " + error)
return
}
// Submit
})Table with Search
searchEntry := widget.NewEntry()
filteredData := []
func updateTable() {
searchTerm := strings.to_lower(searchEntry.Text)
filteredData = []
for _, row := range allData {
if searchTerm == "" || strings.contains(strings.to_lower(row[0]), searchTerm) {
filteredData.append(row)
}
}
table.Refresh()
}
searchEntry.OnSubmitted(func(text) {
updateTable()
})