Basil Parsley Language Reference
Basil web framework extensions to the Parsley language
All syntax verified againstparsv0.2.0 andbasil(January 2026)
Table of Contents
- Connection Literals
- Database Operations
- File I/O Operations
- Format Factories
- HTTP Context (@basil/http)
- Auth Context (@basil/auth)
- Session Methods
- API Helpers (@std/api)
- Dev Tools (@std/dev)
- Server Globals
- Server Functions
- Image Transformation
- 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:
path(String) β File path to the database, or":memory:"for an in-memory databaseoptions(Dictionary, optional):maxOpenConns(Integer) β Maximum number of open connectionsmaxIdleConns(Integer) β Maximum number of idle connections
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:
driverβ"sqlite"inTransactionβtrueif currently in a transactionlastErrorβ Error message from last failed operation, or empty stringsqliteVersionβ SQLite version string (e.g.,"3.45.0")
1.2 PostgreSQL
let db = @postgres("postgres://user:pass@host:5432/dbname")
let db = @postgres("postgres://user:pass@host:5432/dbname", {maxOpenConns: 25})
Arguments:
dsn(String) β PostgreSQL connection string (URL format)options(Dictionary, optional):maxOpenConns(Integer) β Maximum number of open connectionsmaxIdleConns(Integer) β Maximum number of idle connections
Returns: DBConnection object with:
driverβ"postgres"inTransactionβtrueif currently in a transactionlastErrorβ Error message from last failed operation
1.3 MySQL
let db = @mysql("user:pass@tcp(host:3306)/dbname")
let db = @mysql("user:pass@tcp(host:3306)/dbname", {maxOpenConns: 25})
Arguments:
dsn(String) β MySQL connection string in Go database/sql formatoptions(Dictionary, optional):maxOpenConns(Integer) β Maximum number of open connectionsmaxIdleConns(Integer) β Maximum number of idle connections
Returns: DBConnection object with:
driverβ"mysql"inTransactionβtrueif currently in a transactionlastErrorβ Error message from last failed operation
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:
url(String): SFTP URL insftp://user@host:portformat. Thesftp://scheme is required.options(Dictionary, optional):keyFile(path|string) β Path to SSH private key filepassphrase(String) β Passphrase for encrypted private keypassword(String) β Password for password authenticationknownHostsFile(path|string) β Path to known_hosts file for host key verificationtimeout(duration) β Connection timeout
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:
host(String) β Remote hostnameport(Integer) β Port number (default: 22)user(String) β Usernameconnected(Boolean) βtrueif connection is activelastError(String) β Error message from last failed operation
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:
binary(String, required) β Command name or path to executableargs(Array of String, optional) β Command-line argumentsoptions(Dictionary, optional):env(Dictionary) β Environment variables (replaces inherited env if set)dir(path) β Working directorytimeout(duration) β Execution timeout
Returns: Command handle dictionary with:
__typeβ"command"binary(String) β The binary name/pathargs(Array) β The argumentsoptions(Dictionary) β The options
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:
- Left: Command handle from
@shell(...) - Right: Stdin input (
nullfor no input, or string)
Returns: Result dictionary with:
stdout(String) β Standard outputstderr(String) β Standard errorexitCode(Integer) β Exit code (0 = success, -1 = execution failed)error(String|null) β Error message if execution failed,nullotherwise
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:
- Commands are NOT passed through a shellβarguments are passed directly to
exec - Shell metacharacters in arguments are treated as literals (safe from injection)
- Security policy can restrict executable access in production mode
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:
- Left:
DBConnectionobject - Right: SQL string or
<SQL>tag expression
Returns:
Dictionaryβ Row data with column names as keys, values auto-converted:- INTEGER β
Integer - REAL/FLOAT β
Float - TEXT/VARCHAR β
String - BLOB β
Arrayof integers (bytes) - NULL β
null
- INTEGER β
nullβ If no matching row found
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:
- Left:
DBConnectionobject - Right: SQL string or
<SQL>tag expression
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:
- Left:
DBConnectionobject - Right: SQL string or
<SQL>tag expression
Returns: Dictionary with:
affected(Integer) β Number of rows affectedlastId(Integer) β Last inserted row ID (for INSERT statements)
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:
- All attributes are treated as query parameters, bound as positional
?placeholders in declaration order (left-to-right) - Attribute order matters β make sure your attributes appear in the same order as their corresponding
?placeholders in the SQL
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:
soft_delete(String) β Column name for soft delete timestamp. Records won't be physically deleted; instead, this column will be set.
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:
DB-0006β No transaction in progress (for commit/rollback)DB-0007β Already in transaction (for begin)DB-0009β Cannot close managed connection
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:
- Left: Variable name(s) for assignment
- Right: File handle from a format factory
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:
- Left: Value to write (type must match format expectations)
- Right: File handle from a format factory
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:
- Left: Value to append
- Right: File handle (typically
textorlinesformat)
Returns: null on success
3.4 Fetch URL (<=/=)
Fetches content from HTTP/HTTPS URLs.
let data <=/= JSON(@https://api.example.com/users)
Arguments:
- Left: Variable name(s) for assignment
- Right: URL path literal with format factory
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:
- Left: Value to send (becomes request body for HTTP, file content for SFTP)
- Right: Network handle (HTTP request handle or SFTP file handle)
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:
- Left: Value to append
- Right: SFTP file handle
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:
dataβ The successfully read content, ornullon errorerrorβ Error message string, ornullon success
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:
path(path literal or string) β File pathoptions(Dictionary, optional) β Reserved for future use
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:
path(path literal or string) β File pathoptions(Dictionary, optional):header(Boolean) β Iftrue(default), first row is treated as headers and rows are returned as dictionaries. Iffalse, all rows are returned as arrays.
Read Returns:
- With headers:
ArrayofDictionary(column names as keys) - Without headers:
ArrayofArray
Value Auto-Conversion: CSV values are automatically converted:
- Integers (e.g.,
"42") βInteger - Floats (e.g.,
"3.14") βFloat "true"/"false"βBoolean- All others β
String
Write Accepts: Array of Dictionary or Array of Array
4.3 YAML
let handle = YAML(@./config.yaml)
let config <== handle
Arguments:
path(path literal or string) β File pathoptions(Dictionary, optional) β Reserved for future use
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:
path(path literal or string) β File pathoptions(Dictionary, optional) β Reserved for future use (e.g., encoding)
Read Returns: String β Entire file content
Write Accepts: String
4.5 lines
let handle = lines(@./file.txt)
let arr <== handle // Array of strings
Arguments:
path(path literal or string) β File pathoptions(Dictionary, optional) β Reserved for future use
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:
path(path literal or string) β File pathoptions(Dictionary, optional) β Reserved for future use
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:
path(path literal or string) β SVG file pathoptions(Dictionary, optional) β Reserved for future use
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:
path(path literal or string) β Markdown file pathoptions(Dictionary, optional) β Reserved for future use
Read Returns: Dictionary with:
frontmatter(Dictionary) β Parsed YAML frontmatter, or empty dictionary if nonecontent(String) β Markdown rendered to HTML
4.9 dir (Directory)
Creates directory handle for listing contents:
let d = dir(@./data)
let entries <== d
for (entry in entries) {
entry.name
}
Arguments:
path(path literal or string) β Directory path
Read Returns: Array of file/directory handles, each with:
name(String) β Entry nameisDir(Boolean) βtrueif directoryisFile(Boolean) βtrueif file- Format auto-detected from extension (for files)
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:
path(path literal or string) β File path
Auto-detection mapping:
.jsonβ JSON.csvβ CSV.txt,.md,.html,.xml,.parsβ text.logβ lines- All others β text
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:
method(String) β HTTP method:"GET","POST","PUT","DELETE", etc.path(String) β Full request path (e.g.,"/users/123")route(String) β The matched route portion after the handler mount pointquery(Dictionary) β Query string parameters (e.g.,?name=fooβ{name: "foo"})headers(Dictionary) β Request headers (lowercase keys)body(String|Dictionary) β Request body (for POST/PUT). JSON bodies are auto-parsed.form(Dictionary) β Form data (for POST/PUT/PATCH with form-encoded body)files(Dictionary) β Uploaded files (for multipart/form-data requests)
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:
status(Integer) β HTTP status code (default: 200)headers(Dictionary) β Response headers to setcookies(Array) β Cookies to set (array of cookie dictionaries)
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:
name(String, required) β Cookie namevalue(String, required) β Cookie valuepath(String) β Cookie path (default: "/")domain(String) β Cookie domainmaxAge(Integer) β Max age in secondsexpires(String) β Expiry datehttpOnly(Boolean) β HTTP-only flagsecure(Boolean) β Secure flagsameSite(String) β SameSite policy:"Strict","Lax","None"
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:
required(Boolean) βtrueif route requires authenticationuser(Dictionary|null) β Authenticated user, ornullif not logged in
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):
id(Integer) β User IDname(String) β Display nameemail(String) β Email addressrole(String) β User role (e.g.,"admin","user")created(String) β Account creation timestampemail_verified_at(String|null) β Email verification timestampemail_verification_pending(Boolean) βtrueif email not yet verified
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:
key(String) β The session key to retrievedefault(any, optional) β Value to return if key doesn't exist
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:
key(String) β The session keyvalue(any) β Value to store (must be JSON-serializable)
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:
key(String) β The session key to check
Returns: Boolean
session.has("userId") // true or false
7.4 delete(key)
Remove a value from the session.
Arguments:
key(String) β The session key to remove
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:
key(String) β Flash message key (e.g.,"success","error")message(String) β The message content
Returns: null
session.flash("success", "Item saved!")
session.flash("error", "Validation failed")
7.8 getFlash(key)
Retrieve and remove a flash message.
Arguments:
key(String) β Flash message key
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:
url(String or path) β Redirect destination URLstatus(Integer, optional) β HTTP status code (default: 302)
Returns: Redirect object (handled specially by Basil server)
Valid status codes: Any 3xx status code (300β399). Common codes:
- 301 β Moved Permanently
- 302 β Found (default)
- 303 β See Other
- 304 β Not Modified
- 307 β Temporary Redirect
- 308 β Permanent Redirect
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:
message(String, optional) β Custom error message
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:
options(Dictionary, optional) β Additional optionsfn(Function) β The handler function to wrap
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:
fn(Function) β The handler function to wrap
Returns: AuthWrappedFunction
export deleteUser = api.adminOnly(fn(id) {
// Only admin users can access
})
roles(roleList, fn)
Require specific roles to access the handler.
Arguments:
roleList(Array of String) β List of allowed rolesfn(Function) β The handler function to wrap
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:
options(Dictionary, optional) β Additional auth optionsfn(Function) β The handler function to wrap
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:
value(any) β Value to loglabel(String, optional) β Label for the log entryoptions(Dictionary, optional):level(String) β Log level:"info"(default),"warn","error"
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:
route(String) β Route identifier (alphanumeric,-,_only β no/or other special characters)value(any) β Value to loglabel(String, optional) β Label for the log entryoptions(Dictionary, optional):level(String) β Log level:"info","warn","error"
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:
route(String) β Route identifier (alphanumeric,-,_only), or empty string to reset
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:
route(String) β Route identifier (alphanumeric,-,_only)
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:
path(path literal or String) β Path to the file to register
Returns: String β Public URL with content hash for cache-busting
Errors:
stateerror β If called outside Basil server contextsecurityerror β If path is outside handler directoryIO-0001β If file cannot be read
let logoUrl = publicUrl(@./assets/logo.svg)
<img src={logoUrl} alt="Logo"/>
// Produces: /assets/logo-a1b2c3d4.svg
How it works:
- File content is hashed
- File is copied to public assets with hash in filename
- 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:
- Basil generates a unique CSRF token per session
- Token is stored in a cookie and available via
basil.csrf.token - POST/PUT/DELETE requests must include the token as
_csrfparameter - 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:
classβ Error category (e.g.,"database","io","security")codeβ Specific error code (e.g.,"DB-0002","IO-0003")messageβ Human-readable error descriptionhintsβ Array of suggestions for fixing the error
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 | β |