Basil Parsley Language Reference

Basil web framework extensions to the Parsley language
All syntax verified against pars v0.2.0 and basil (January 2026)

Table of Contents

  1. Connection Literals
  2. Database Operations
  3. File I/O Operations
  4. Format Factories
  5. HTTP Context (@basil/http)
  6. Auth Context (@basil/auth)
  7. Session Methods
  8. API Helpers (@std/api)
  9. Dev Tools (@std/dev)
  10. Server Globals
  11. Server Functions
  12. Image Transformation
  13. Error Handling

1. Connection Literals

Connection literals create database and service connections.

1.1 SQLite

let db = @sqlite("./database.db")
let db = @sqlite(":memory:")
let db = @sqlite("./database.db", {maxOpenConns: 10})

Arguments:

Connection Settings: SQLite connections automatically use WAL mode and a 5000ms busy timeout for better concurrency (except for :memory: databases where WAL is not supported).

Returns: DBConnection object with the following properties:

1.2 PostgreSQL

let db = @postgres("postgres://user:pass@host:5432/dbname")
let db = @postgres("postgres://user:pass@host:5432/dbname", {maxOpenConns: 25})

Arguments:

Returns: DBConnection object with:

1.3 MySQL

let db = @mysql("user:pass@tcp(host:3306)/dbname")
let db = @mysql("user:pass@tcp(host:3306)/dbname", {maxOpenConns: 25})

Arguments:

Returns: DBConnection object with:

1.4 SFTP

Creates an SFTP connection for remote file operations over SSH.

let sftp = @sftp("sftp://user@host:22")
let sftp = @sftp("sftp://user:password@host:22")
let sftp = @sftp("sftp://user@host:22", {keyFile: @~/.ssh/id_rsa})

Arguments:

Authentication: Supports SSH key authentication (via keyFile option) or password authentication (via URL or password option). At least one authentication method must be provided.

Returns: SFTPConnection object with properties:

Errors: SEC-0006 (no authentication method provided), NET-0003 (SSH connection failed), NET-0008 (host key verification failed), NET-0009 (SFTP client failed)

SFTP Connection Methods

Method Arguments Returns Description
.close() none null Close the connection

SFTP File Operations

Access remote files by indexing the connection:

let sftp = @sftp("sftp://user@host:22", {keyFile: @~/.ssh/id_rsa})
let remoteFile = sftp[@./path/to/file.txt]

This creates an SFTPFileHandle with methods:

Method Arguments Returns Description
.mkdir(options?) {parents?: boolean} null Create directory (parents: true for recursive)
.rmdir(options?) {recursive?: boolean} null Remove directory
.remove() none null Remove file

Reading/Writing Remote Files:

let sftp = @sftp("sftp://user@host:22", {keyFile: @~/.ssh/id_rsa})

// Read remote file
let content <== text(sftp[@./remote/file.txt])

// Write to remote file
"data" =/=> text(sftp[@./remote/file.txt])

// Create remote directory
sftp[@./remote/newdir].mkdir({parents: true})

1.5 Shell

Creates command handles for executing external programs.

@shell("binary")
@shell("binary", ["arg1", "arg2"])
@shell("binary", ["args"], {env: {...}, dir: @./path, timeout: @30s})

Arguments:

Returns: Command handle dictionary with:

Note: @shell(...) creates a command handle but does not execute it. Use <=#=> to execute.

Execute Operator (<=#=>)

Executes a command handle with optional stdin input.

let result = @shell("echo", ["hello"]) <=#=> null
let result = @shell("cat") <=#=> "stdin input"

Arguments:

Returns: Result dictionary with:

Examples:

// Simple command
let result = @shell("echo", ["Hello, World!"]) <=#=> null
result.stdout                   // "Hello, World!\n"
result.exitCode                 // 0

// Command with environment
let result = @shell("printenv", ["MY_VAR"], {
    env: {MY_VAR: "test"}
}) <=#=> null
result.stdout                   // "test\n"

// Command with stdin
let result = @shell("cat") <=#=> "piped input"
result.stdout                   // "piped input"

// Command with timeout
let result = @shell("sleep", ["10"], {timeout: @2s}) <=#=> null
// Kills process after 2 seconds

// Check for errors
if (result.exitCode != 0) {
    `Command failed: {result.stderr}`
}

Security:

Errors: CMD-0001 (missing field), CMD-0002 (wrong type), CMD-0003 (invalid args), CMD-0004 (invalid stdin type)

1.6 Server Database (@DB)

In Basil handlers, @DB refers to the configured server database:

// In basil.yaml: database.path: "./app.db"
let users = @DB <=??=> "SELECT * FROM users"

Note: @DB is a managed connectionβ€”scripts cannot close it.


2. Database Operations

All database operators require a DBConnection on the left-hand side.

2.1 Query One Row (<=?=>)

Returns a single row as a dictionary, or null if not found.

let db = @sqlite("./test.db")
let user = db <=?=> "SELECT * FROM users WHERE id = 1"
user.name                       // "Alice"

Arguments:

Returns:

Errors: DB-0002 (query failed), DB-0004 (scan failed), DB-0008 (column error)

2.2 Query Many Rows (<=??=>)

Returns an array of dictionaries.

let users = db <=??=> "SELECT * FROM users ORDER BY id"
users.length()                  // 2
users[0].name                   // "Alice"

Arguments:

Returns: Array of Dictionary objects, each representing a row. Empty array if no rows match.

Errors: DB-0002 (query failed), DB-0004 (scan failed), DB-0008 (column error)

2.3 Execute Mutation (<=!=>)

Executes INSERT, UPDATE, DELETE, or DDL statements.

let result = db <=!=> "INSERT INTO users (name, age) VALUES ('Bob', 25)"
result.affected                 // 1
result.lastId                   // 3

Arguments:

Returns: Dictionary with:

Important: Execute statements must be assigned to a variable (even _):

let _ = db <=!=> "DELETE FROM users WHERE id = 5"

Errors: DB-0011 (execute failed)

2.4 Parameterized Queries

Use template strings for safe parameterization:

let name = "Alice"
let user = db <=?=> `SELECT * FROM users WHERE name = '{name}'`

Or use the <SQL> tag for complex queries with positional parameters:

let SearchUsers = fn(term) {
    <SQL name={term}>
        "SELECT * FROM users WHERE name LIKE ?"
    </SQL>
}
let users = db <=??=> <SearchUsers term="Ali%"/>

<SQL> Tag Attributes:

Note: Template strings are convenient but the <SQL> tag provides explicit parameter binding when needed.

2.5 Database Connection Methods

Method Arguments Returns Description
.begin() none boolean Begin a transaction
.commit() none boolean Commit the current transaction
.rollback() none boolean Rollback the current transaction
.close() none null Close the connection (not allowed on managed connections)
.ping() none boolean Test if connection is alive
.lastInsertId() none integer Get last inserted row ID (SQLite only)
.createTable(schema, name?) schema: Schema, name?: string boolean Create table from schema if not exists
.bind(schema, name, opts?) schema: Schema, name: string, opts?: dict TableBinding Bind schema to table

createTable Details

db.createTable(UserSchema)           // Table name defaults to "userschemas" (pluralized lowercase)
db.createTable(UserSchema, "users")  // Explicit table name

When no table name is provided, it defaults to the schema name lowercased and pluralized with "s".

bind Options

The bind method accepts an optional third argument for configuration:

let Users = db.bind(UserSchema, "users", {soft_delete: "deleted_at"})

Options:

Transaction Example

let db = @sqlite("./app.db")

db.begin()
let _ = db <=!=> "INSERT INTO users (name) VALUES ('Alice')"
let _ = db <=!=> "INSERT INTO orders (user_id) VALUES (1)"
db.commit()
// Or: db.rollback() to cancel

Note: Managed connections (@DB) cannot be closed by scripts.

Errors:

Schema Binding Example

let UserSchema = schema {
    name: string
    email: string
    age: integer?
}

// Bind schema to table for CRUD operations
let Users = db.bind(UserSchema, "users")

// Use table binding (see Schema-Table Binding documentation)
let user = Users.find(1)

3. File I/O Operations

File I/O operators work with file handles created by format factories.

3.1 Read File (<==)

Reads file content based on format factory.

// Read as text
let content <== text(@./file.txt)

// Read as JSON
let data <== JSON(@./config.json)

// Read as lines array
let lines <== lines(@./log.txt)

// Read directory listing
let entries <== dir(@./data)

Arguments:

Returns: Content type depends on the format factory used (see Section 4).

Errors: IO-0003 (read failed), FILEOP-0007 (invalid handle)

3.2 Write File (==>)

Writes content to file, overwriting existing content.

"Hello, World!" ==> text(@./output.txt)
{name: "Alice"} ==> JSON(@./data.json)

Arguments:

Returns: null on success

Errors: IO-0004 (write failed)

3.3 Append to File (==>>)

Appends content to existing file.

"Log entry\n" ==>> text(@./app.log)

Arguments:

Returns: null on success

3.4 Fetch URL (<=/=)

Fetches content from HTTP/HTTPS URLs.

let data <=/= JSON(@https://api.example.com/users)

Arguments:

Returns: Parsed response body (type depends on format factory)

Errors: NET-0002 (request failed), NET-0004 (non-2xx status)

3.5 Remote Write (=/=>)

Sends data to a network target (HTTP endpoint or SFTP server).

Grammar: expression =/=> expression

{name: "Alice"} =/=> JSON(@https://api.example.com/users)

Arguments:

HTTP behavior: Defaults method to POST. Use .put or .patch accessors for other methods. Returns: Response dictionary (HTTP) or null (SFTP success) or error

Errors: NET-0002 (request failed)

3.6 Remote Append (=/=>>)

Appends data to a remote file via SFTP.

Grammar: expression =/=>> expression

"log entry\n" =/=>> text(sftp[@./var/log/app.log])

Arguments:

Not supported for HTTP targets (HTTP has no append semantic).

Returns: null on success

3.7 Error Capture Pattern

Use {data, error} destructuring to capture errors instead of failing:

let {data, error} <== JSON(@./maybe-missing.json)
if (error) {
    "File not found or invalid JSON"
} else {
    data.value
}

With error capture:

This pattern prevents script termination on I/O errors, allowing graceful handling.


4. Format Factories

Format factories create file handles with specific read/write formats. All format factories accept a path literal as the first argument and an optional options dictionary as the second argument.

4.1 JSON

let handle = JSON(@./data.json)
let data <== handle

Arguments:

Read Returns: Parsed JSON value (Dictionary, Array, String, Integer, Float, Boolean, or null)

Write Accepts: Any JSON-serializable value

4.2 CSV

let handle = CSV(@./data.csv)
let rows <== handle             // Array of dictionaries
rows[0].name                    // First row, "name" column

Arguments:

Read Returns:

Value Auto-Conversion: CSV values are automatically converted:

Write Accepts: Array of Dictionary or Array of Array

4.3 YAML

let handle = YAML(@./config.yaml)
let config <== handle

Arguments:

Read Returns: Parsed YAML value (same types as JSON)

Write Accepts: Any YAML-serializable value

4.4 text

let handle = text(@./readme.txt)
let content <== handle          // String

Arguments:

Read Returns: String β€” Entire file content

Write Accepts: String

4.5 lines

let handle = lines(@./file.txt)
let arr <== handle              // Array of strings

Arguments:

Read Returns: Array of String β€” One string per line (trailing empty line removed)

Write Accepts: Array of String (joined with newlines)

4.6 raw

let handle = raw(@./binary.dat)
let data <== handle             // Array of integers (0-255)

Arguments:

Read Returns: Array of Integer β€” Each byte as an integer (0–255)

Write Accepts: Array of Integer (values 0–255)

4.7 SVG

Reads SVG files, stripping XML prolog for direct HTML embedding:

let icon <== SVG(@./icon.svg)
<div class="icon">{icon}</div>

Arguments:

Read Returns: String β€” SVG content without XML declaration, ready for HTML embedding

4.8 MD (Markdown)

Reads markdown files with optional YAML frontmatter:

let doc <== MD(@./post.md)
doc.frontmatter.title           // YAML frontmatter as dictionary
doc.content                     // HTML content (rendered from markdown)

Arguments:

Read Returns: Dictionary with:

4.9 dir (Directory)

Creates directory handle for listing contents:

let d = dir(@./data)
let entries <== d
for (entry in entries) {
    entry.name
}

Arguments:

Read Returns: Array of file/directory handles, each with:

4.10 file (Auto-detect)

Auto-detects format from file extension:

let data <== file(@./config.json)   // Detected as JSON
let text <== file(@./readme.txt)    // Detected as text

Arguments:

Auto-detection mapping:


5. HTTP Context (@basil/http)

Server-only: Available in Basil request handlers.

let {request, response, route, method} = import @basil/http

All exports are dynamic accessors that always return the current request's values, even when imported at module scope.

5.1 request

Access to the current HTTP request.

Type: Dictionary

Properties:

Example:

let {request} = import @basil/http
request.method                  // "GET"
request.query.page ?? 1         // Query param with default

5.2 response

Set response headers, status, and cookies.

Type: Dictionary (mutable)

Properties:

Example:

let {response} = import @basil/http
response.status = 201
response.headers["X-Custom"] = "value"
response.cookies = [{name: "session", value: "abc123", httpOnly: true}]

Cookie Dictionary Properties:

5.3 route

The matched route path (portion after handler mount point).

Type: String

Example:

let {route} = import @basil/http
// Handler mounted at /users, request to /users/123
route                           // "123"

5.4 method

Current HTTP method (shortcut for request.method).

Type: String

Example:

let {method} = import @basil/http
if (method == "POST") {
    // Handle form submission
}

6. Auth Context (@basil/auth)

Server-only: Available when auth is configured in basil.yaml.

let {session, auth, user} = import @basil/auth

All exports are dynamic accessors for per-request freshness.

6.1 session

Current session module (see Section 7 for methods).

Type: SessionModule

6.2 auth

Authentication context dictionary.

Type: Dictionary

Properties:

Example:

let {auth} = import @basil/auth
if (auth.required && !auth.user) {
    redirect("/login")
}

6.3 user

Shortcut to auth.userβ€”the currently authenticated user.

Type: Dictionary or null

User Dictionary Properties (when authenticated):

Example:

let {user} = import @basil/auth
if (user) {
    `Welcome, {user.name}!`
} else {
    "Please log in"
}

7. Session Methods

Server-only: Available via @basil/auth session.

let {session} = import @basil/auth

Session data is automatically persisted between requests (stored in encrypted cookies by default).

7.1 get(key, default?)

Retrieve a value from the session.

Arguments:

Returns: Stored value, default if provided and key missing, or null

session.get("userId")           // 123 or null
session.get("theme", "light")   // "light" if not set

7.2 set(key, value)

Store a value in the session.

Arguments:

Returns: null

session.set("userId", 123)
session.set("cart", [{id: 1, qty: 2}])

7.3 has(key)

Check if a key exists in the session.

Arguments:

Returns: Boolean

session.has("userId")           // true or false

7.4 delete(key)

Remove a value from the session.

Arguments:

Returns: null

session.delete("userId")

7.5 clear()

Remove all session data (including flash messages).

Arguments: None

Returns: null

session.clear()

7.6 all()

Get all session data as a dictionary.

Arguments: None

Returns: Dictionary β€” All key-value pairs in the session

let data = session.all()        // {userId: 123, theme: "dark", ...}

7.7 flash(key, message)

Set a flash message (one-time message, cleared after retrieval).

Arguments:

Returns: null

session.flash("success", "Item saved!")
session.flash("error", "Validation failed")

7.8 getFlash(key)

Retrieve and remove a flash message.

Arguments:

Returns: String (the message) or null if not set

let msg = session.getFlash("success")  // "Item saved!" (then cleared)

7.9 hasFlash()

Check if any flash messages exist.

Arguments: None

Returns: Boolean

if (session.hasFlash()) {
    // Display flash messages
}

7.10 getAllFlash()

Retrieve and remove all flash messages.

Arguments: None

Returns: Dictionary β€” All flash messages (then cleared)

let flashes = session.getAllFlash()    // {success: "...", error: "..."}

7.11 regenerate()

Regenerate session ID (recommended after login for security).

Arguments: None

Returns: null

// After successful login
session.set("userId", user.id)
session.regenerate()            // Prevent session fixation attacks

8. API Helpers (@std/api)

import @std/api
// or
let {redirect, notFound, forbidden} = import @std/api

8.1 redirect(url, status?)

Create an HTTP redirect response.

Arguments:

Returns: Redirect object (handled specially by Basil server)

Valid status codes: Any 3xx status code (300–399). Common codes:

Errors: VALUE-0001 (empty URL), VALUE-0002 (invalid status code β€” must be 3xx)

redirect("/dashboard")          // 302 Found (default)
redirect("/dashboard", 301)     // 301 Permanent Redirect
redirect("/other", 303)         // 303 See Other
redirect("/temp", 307)          // 307 Temporary Redirect
redirect("/perm", 308)          // 308 Permanent Redirect

8.2 Error Responses

All error functions create APIError objects that Basil converts to appropriate HTTP responses.

Common Signature:

Returns: APIError object with code, message, and status properties

notFound(message?)

notFound()                      // 404, "Not found"
notFound("User not found")      // 404, custom message

forbidden(message?)

forbidden()                     // 403, "Forbidden"
forbidden("Access denied")

badRequest(message?)

badRequest()                    // 400, "Bad request"
badRequest("Invalid input")

unauthorized(message?)

unauthorized()                  // 401, "Unauthorized"
unauthorized("Please log in")

conflict(message?)

conflict()                      // 409, "Conflict"
conflict("Resource already exists")

serverError(message?)

serverError()                   // 500, "Internal server error"
serverError("Something went wrong")

8.3 Auth Wrappers

Wrap handler functions with auth requirements. These decorators add metadata that Basil checks before invoking the handler.

public(fn) / public(options, fn)

Mark a handler as publicly accessible (no authentication required).

Arguments:

Returns: AuthWrappedFunction

let api = import @std/api

export listProducts = api.public(fn() {
    // Anyone can access
})

adminOnly(fn)

Require admin role to access the handler.

Arguments:

Returns: AuthWrappedFunction

export deleteUser = api.adminOnly(fn(id) {
    // Only admin users can access
})

roles(roleList, fn)

Require specific roles to access the handler.

Arguments:

Returns: AuthWrappedFunction

export editContent = api.roles(["editor", "admin"], fn() {
    // Only editors or admins can access
})

auth(fn) / auth(options, fn)

Require authentication (any logged-in user).

Arguments:

Returns: AuthWrappedFunction

export profile = api.auth(fn() {
    // Any authenticated user can access
})

9. Dev Tools (@std/dev)

let {dev} = import @std/dev

Dev tools are for development debugging. All functions are no-ops in production mode (when dev: false in basil.yaml) and when running in pars CLI outside a Basil server context, so they can safely remain in production code.

Important: Route validation only occurs when dev tools are active (inside Basil server with dev mode enabled). In pars CLI or production mode, all dev functions silently return null without validation.

9.1 dev.log(value) / dev.log(label, value) / dev.log(label, value, options)

Log values to Basil's dev panel.

Arguments:

Returns: null

dev.log(someValue)                      // Log value
dev.log("user", currentUser)            // Log with label
dev.log("warning", data, {level: "warn"}) // Log as warning
dev.log("error!", err, {level: "error"})  // Log as error

9.2 dev.clearLog()

Clear all dev log entries for the current route.

Arguments: None

Returns: null

dev.clearLog()

9.3 dev.logPage(route, value) / dev.logPage(route, label, value, options?)

Log to a specific route's dev panel.

Arguments:

Returns: null

Errors: VAL-0009 (invalid route format)

dev.logPage("/admin", "debug info")
dev.logPage("/admin", "users", userList)
dev.logPage("/admin", "error", err, {level: "error"})

9.4 dev.setLogRoute(route)

Set default route for subsequent dev.log() calls.

Arguments:

Returns: null

Errors: VAL-0009 (invalid route format)

dev.setLogRoute("/dashboard")
dev.log("now goes to /dashboard")
dev.setLogRoute("")              // Reset to current route

9.5 dev.clearLogPage(route)

Clear logs for a specific route.

Arguments:

Returns: null

Errors: VAL-0009 (invalid route format β€” route contains invalid characters)

dev.clearLogPage("/admin")

10. Server Globals

10.1 @params

Server-only: Merged URL query parameters and form data.

Type: Dictionary

Query parameters and POST form data are merged, with form data taking precedence on conflicts.

// URL: /search?q=hello
// Or POST form: q=hello
@params.q                       // "hello"
@params["page"] ?? 1            // Default to 1 if missing

Note: Use @params instead of request.query for unified access to both query strings and form submissions.

10.2 @env

Environment variables dictionary. Works in both pars CLI and Basil server.

Type: Dictionary

@env.HOME                       // "/Users/alice"
@env["DATABASE_URL"]            // Connection string
@env.PATH                       // System PATH

Note: Environment variables are read at startup and are read-only.

10.3 @args

Command-line arguments array. Primarily useful in pars CLI scripts.

Type: Array of String

// pars script.pars arg1 arg2
@args[0]                        // "arg1"
@args[1]                        // "arg2"
@args.length()                  // 2

Note: In Basil server context, @args is typically empty.


11. Server Functions

11.1 publicUrl(path)

Server-only: Register a file and get its content-hashed public URL.

Arguments:

Returns: String β€” Public URL with content hash for cache-busting

Errors:

let logoUrl = publicUrl(@./assets/logo.svg)
<img src={logoUrl} alt="Logo"/>
// Produces: /assets/logo-a1b2c3d4.svg

How it works:

  1. File content is hashed
  2. File is copied to public assets with hash in filename
  3. URL with hash is returned for cache-busting

Security: The path must be within the handler's root directory. Path traversal attempts are rejected.

11.2 CSRF Token

Access CSRF token via the basil context for form protection.

Available via: basil.csrf.token in the request context

<form method="post">
    <input type="hidden" name="_csrf" value={basil.csrf.token}/>
    // form fields...
    <button type="submit">Submit</button>
</form>

How it works:

  1. Basil generates a unique CSRF token per session
  2. Token is stored in a cookie and available via basil.csrf.token
  3. POST/PUT/DELETE requests must include the token as _csrf parameter
  4. Basil validates the token and rejects mismatched requests

Note: CSRF protection is automatic for state-changing HTTP methods. Always include the token in forms.


12. Image Transformation

Basil provides four built-in functions for transforming, optimising, and serving images. Images are transformed on first use, cached to disk at content-hashed URLs, and served with immutable cache headers.

12.1 image()

image(path) β†’ string
image(path, options) β†’ string

Transforms an image and returns its public URL. The transform runs once; all subsequent calls return the cached URL immediately.

// Serve original (auto-rotated, metadata stripped)
let url = image(@./photo.jpg)

// Resize to 800px wide, convert to WebP
let url = image(@./photo.jpg, {width: 800, format: "webp"})

// Crop to exact 400Γ—300 (center crop)
let thumb = image(@./photo.jpg, {width: 400, height: 300, crop: "center"})

// Smart crop β€” focuses on faces and high-interest regions
let thumb = image(@./photo.jpg, {width: 400, height: 300, crop: "smart"})

// Smart scale β€” content-aware resize via seam carving
let narrow = image(@./banner.jpg, {width: 600, scale: "smart"})

Options:

Key Type Default Description
width integer β€” Target width in pixels
height integer β€” Target height in pixels
crop string "" "center" fills the exact box. "smart" analyses for faces/interest regions and crops to preserve them. Without crop, image fits within box.
scale string "" "smart" resizes via seam carving (content-aware). Cannot combine with crop.
focal dict β€” {x, y} or {x, y, w, h} (normalised 0–1). Requires crop: "smart".
quality integer format default 1–100. Defaults: JPEG 85, WebP 80, PNG lossless
format string source format "jpeg", "png", "webp", "gif"
sharpen bool or number true Auto-sharpen on downscale. false disables. A number sets sigma explicitly.

EXIF orientation is applied automatically. Images are never upscaled β€” requesting a width larger than the source clamps to source dimensions.

scale: "smart" allows upscaling (bypasses the default no-upscale clamp). Smart crop requires both width and height. Seam carving changes beyond 30% may produce artifacts.

12.2 imageInfo()

imageInfo(path) β†’ dict

Returns image metadata without transforming the image. Results are cached in memory (keyed by path and modification time).

let info = imageInfo(@./hero.jpg)
// {width: 3024, height: 4032, format: "jpeg", orientation: "portrait"}
Key Type Description
width integer Width in pixels (after EXIF auto-orientation)
height integer Height in pixels (after EXIF auto-orientation)
format string "jpeg", "png", "webp", or "gif"
orientation string "landscape", "portrait", or "square"

12.3 imageBlur()

imageBlur(path) β†’ string

Generates a Low Quality Image Placeholder (LQIP) and returns it as an inline data: URI (~600 bytes). Use as a CSS background image that appears instantly while the full image loads.

let blur = imageBlur(@./hero.jpg)
let full = image(@./hero.jpg, {width: 1200})

<div style={"background-image: url(" + blur + "); background-size: cover;"}>
    <img src={full} loading=lazy alt="Hero"/>
</div>

12.4 imageSrcset()

imageSrcset(path, style, widths) β†’ dict
imageSrcset(path, style, scales, "x") β†’ dict

Generates multiple resized variants and returns a dict for responsive <img> tags.

// Width descriptor mode
let resp = imageSrcset(@./hero.jpg, {format: "webp"}, [400, 800, 1200])

<img
    src={resp.src}
    srcset={resp.srcset}
    sizes="(max-width: 600px) 100vw, 800px"
    width={resp.width}
    height={resp.height}
    alt="Hero"
/>
// Density descriptor mode (requires style.width)
let resp = imageSrcset(@./logo.png, {width: 120}, [1, 2, 3], "x")

Returns: {src: string, srcset: string, width: integer, height: integer}

12.5 Supported Formats

Format Input Output
JPEG βœ… βœ…
PNG βœ… βœ…
GIF βœ… (first frame) βœ…
WebP βœ… βœ…

12.6 Configuration

images:
  cache_dir: ./cache/images   # Default cache location
  max_width: 4000
  max_height: 4000
  default_quality: 85
  default_format: ""

See the Images manual page for full documentation.


13. Error Handling

13.1 Error Object Structure

When operations fail, Basil returns an Error object with:

13.2 Error Classes

Class Description
database Database connection/query errors
io File I/O errors
network HTTP/SFTP network errors
security Permission/access denied errors
type Type mismatch errors
value Invalid value errors
state Invalid state errors

13.3 Common Error Codes

Database Errors (DB-0xxx)

Code Description
DB-0002 Query execution failed
DB-0003 Failed to open database
DB-0004 Failed to scan row
DB-0006 No transaction in progress
DB-0007 Already in transaction
DB-0009 Cannot close managed connection
DB-0011 Execute statement failed
DB-0012 Invalid operand type for database operator

I/O Errors (IO-0xxx)

Code Description
IO-0001 General file operation failed
IO-0002 Module not found
IO-0003 Failed to read file
IO-0004 Failed to write file
IO-0005 Failed to delete file
IO-0006 Failed to create directory

Network Errors (NET-0xxx)

Code Description
NET-0001 Network operation failed
NET-0002 HTTP request failed
NET-0003 SSH connection failed
NET-0004 Non-2xx HTTP status returned
NET-0006 Failed to read SSH key file
NET-0007 Failed to parse SSH key
NET-0008 Host key verification failed
NET-0009 SFTP client creation failed

Security Errors (SEC-0xxx)

Code Description
SEC-0001 General access denied
SEC-0002 Read access denied
SEC-0003 Write access denied
SEC-0004 Execute access denied
SEC-0005 Network access denied
SEC-0006 No authentication method provided (SFTP)

13.4 Error Handling Patterns

Pattern 1: Error Capture with File I/O

Use destructuring to capture errors without script termination:

let {data, error} <== JSON(@./config.json)
if (error) {
    // Handle gracefully
    let config = {defaults: true}
} else {
    let config = data
}

Pattern 2: Null Checks for Database Queries

Single-row queries return null when no match is found:

let user = db <=?=> `SELECT * FROM users WHERE id = {id}`
if (!user) {
    notFound("User not found")
} else {
    <UserProfile user={user}/>
}

Pattern 3: Empty Array for Multi-Row Queries

Multi-row queries return an empty array when no matches:

let posts = db <=??=> "SELECT * FROM posts WHERE published = true"
if (posts.length() == 0) {
    <p>No posts yet.</p>
} else {
    for (post in posts) {
        <PostCard post={post}/>
    }
}

Pattern 4: API Error Responses

Use @std/api helpers to return appropriate HTTP errors:

import @std/api

let user = db <=?=> `SELECT * FROM users WHERE id = {id}`
if (!user) {
    notFound("User not found")
}

if (user.role != "admin") {
    forbidden("Admin access required")
}

Appendix A: Feature Availability

Feature pars CLI Basil Server Notes
File I/O (<==, ==>, ==>>) βœ“ βœ“
URL Fetch (<=/=) βœ“ βœ“ Requires --allow-net in pars
Remote Write (=/=>, =/=>>) βœ“ βœ“ Requires --allow-net in pars
Database operators βœ“ βœ“
Format factories βœ“ βœ“
@env βœ“ βœ“
@args βœ“ β€” Empty in server context
@std/api βœ“ βœ“ Returns objects in pars, handled by server
@std/dev no-op βœ“ Dev panel requires server; silently returns null in pars
@basil/http β€” βœ“ Request context only
@basil/auth β€” βœ“ Requires auth config
@params β€” βœ“ Query + form data
@DB β€” βœ“ Server-configured database
Session methods β€” βœ“ Cookie-based sessions
publicUrl() β€” βœ“ Asset registration
CSRF token β€” βœ“ Form protection

Appendix B: Type Summary

Type Description Example
DBConnection Database connection handle @sqlite("./db.sqlite")
SFTPConnection SFTP connection handle @sftp("sftp://user@host:22", {...})
SessionModule Session data wrapper import @basil/auth
Redirect HTTP redirect response redirect("/path")
APIError HTTP error response notFound()
AuthWrappedFunction Auth-decorated function api.public(fn() {...})
File Handle File read/write handle JSON(@./data.json)
Directory Handle Directory listing handle dir(@./data)

Appendix C: Format Factory Summary

Factory Read Type Write Type Options
JSON Dictionary/Array/... any JSON-serializable β€”
CSV Array of Dict/Array Array of Dict/Array {header: bool}
YAML Dictionary/Array/... any YAML-serializable β€”
text String String β€”
lines Array of String Array of String β€”
bytes Array of Integer Array of Integer β€”
SVG String (no prolog) β€” β€”
MD Dict (frontmatter, content) β€” β€”
dir Array of handles β€” β€”
file auto-detected auto-detected β€”