Web Guide¶
Build web apps with EPL's native Create WebApp runtime. This is the authoritative served web path used by epl serve, deployment generation, and maintained starter templates.
Quick Start¶
Create WebApp called app
Route "/" shows
Page "Hello EPL"
Heading "Hello from EPL"
Text "This page is served by the native EPL web runtime."
Link "Health API" to "/api/health"
End
End
Route "/api/health" responds with
Send json Map with status = "ok" and service = "demo"
End
Start app on port 8000
When epl serve hosts the file, the CLI can control the port. Start app on port ... is useful in direct-run standalone programs.
Starter Templates¶
epl new mysite --template web
epl new myapi --template api
epl new myauth --template auth
epl new mybot --template chatbot
epl new myui --template frontend
epl new myapp --template fullstack
Review generated starters before production deployment. They are scaffolds, not a substitute for your security and operations policy.
Native Route Syntax¶
Page route:
Route "/" shows
Page "Welcome"
Heading "Welcome"
Text "Server-rendered EPL page."
Link "Users API" to "/api/users"
End
End
JSON route:
Route "/api/users" responds with
users = ["Alice", "Bob"]
Send json Map with users = users and count = length(users)
End
Path parameters — both the colon form (:name) and the brace form ({name})
are supported and equivalent:
Route "/users/:name" responds with
Send json Map with name = name
End
Route "/users/{id}" responds with
Send json Map with id = id
End
Each captured parameter is available three ways inside the route body: as a
bare variable (name, id), via request_params.name, and via
web_request_param("name"). The bare-variable form matches the syntax used
throughout the example apps.
Redirects¶
Issue an HTTP redirect with either the standalone statement or the Send alias:
Request Context Variables¶
Inside native WebApp routes, EPL exposes:
| Variable | Description |
|---|---|
request_data |
Parsed body map for JSON/form requests |
request_params |
Merged path and query parameters |
request_headers |
Request headers map |
request_method |
HTTP method |
request_path |
Normalized request path |
request |
Combined request object |
session_id |
Current session identifier when present |
Example:
Route "/users/:name" responds with
name = request_params.name
role = request_data.get("role", "guest")
Send json Map with name = name and role = role and path = request_path
End
Dynamic Route State¶
Variables defined before Page or Send json can be used inside the route.
Route "/hello/:name" shows
title = "Welcome, " + request_params.name
Page "$title"
Heading "$title"
Text "Served with method $request_method"
End
End
Page Elements¶
Page "Dashboard"
Heading "Dashboard"
Subheading "Today"
Text "Status: $status"
Link "Home" to "/"
Button "Refresh" class "primary"
Div class "panel"
Text "Nested content"
End
End
Supported HTML DSL areas include headings, text, links, images, buttons, forms, structural elements, styles, event helpers, and raw HTML. Use Raw HTML only for trusted static markup.
Never pass user-controlled input into Raw HTML.
Forms and inputs¶
Input is positional (Input "field_name"), and Form takes an action
(method defaults to POST):
Page "Add Task"
Form action "/add"
Input "title" placeholder "What needs doing?"
Button "Save"
End
End
This renders <form method="POST" action="/add"> with a text <input name="title">.
Stylesheet (raw CSS)¶
Stylesheet ... End is a raw-CSS block — its body is literal CSS injected
verbatim into the page <head> (not a container for nested Style selectors):
Page "Styled"
Stylesheet
.card { border-radius: 12px; padding: 16px; }
.card:hover { box-shadow: 0 4px 12px rgba(0,0,0,.1); }
End
Div class "card"
Text "Hover me"
End
End
Dynamic lists and conditionals (For each / If)¶
For each and If work directly inside a Page — their bodies are page
elements, and they are expanded into markup per request against the route's
data. This is how you render a dynamic list without dropping to Raw HTML:
Route "/tasks" shows
tasks = db_query(db, "SELECT id, title, done FROM tasks ORDER BY id")
Page "Tasks"
Heading "My Tasks"
For each t in tasks
Div class "row"
Text "$t.title"
If t.done == 1 Then
Text "✓ done"
Otherwise
Link "Complete" to "/done/$t.id"
End
End
End
End
End
The loop variable (t) is in scope for every element in the body, including
nested containers. An empty list renders nothing; For i from 1 to N and
Otherwise branches are supported the same way.
Database Integration¶
Use built-in SQLite helpers directly for the stable path.
Create WebApp called app
db = db_open("notes.db")
db_execute(db, "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL)")
Route "/api/notes" responds with
notes = db_query(db, "SELECT id, title FROM notes ORDER BY id")
Send json Map with ok = True and notes = notes
End
db_query always returns a list of row maps. For a single value (e.g. a
count(*) or a lookup by id), use db_query_one, which returns the first row
map (or null), so property access works directly:
row = db_query_one(db, "SELECT count(*) as count FROM notes")
total = row.count
Route "/api/notes/create" responds with
title = trim(request_data.get("title", ""))
If length(title) == 0 Then
Send json Map with ok = False and error = "Title is required"
Otherwise
db_execute(db, "INSERT INTO notes (title) VALUES (?)", [title])
Send json Map with ok = True
End
End
db_create_table accepts standard column constraints (INTEGER PRIMARY KEY, TEXT NOT NULL, VARCHAR(255), …), so it is fine for most tables. For foreign keys, indexes, and DEFAULT/CHECK clauses, use versioned SQL migrations through db_execute. See the database guide for the full boundary.
Authentication Helpers¶
EPL exposes auth helpers directly in the runtime:
password_hash = auth_hash_password("secret-password")
ok = auth_verify_password("secret-password", password_hash)
token = auth_generate_token(32)
JWT pattern:
secret = env_get("JWT_SECRET", "")
payload = Map with user_id = 123 and role = "admin"
token = auth_jwt_create(payload, secret, 3600)
verified = auth_jwt_verify(token, secret)
Secrets and API keys (.env)¶
Read every secret from the environment with env_get("NAME", "default") — never
hardcode it in source. EPL auto-loads a .env file from the program's
directory (and the current working directory) before the program runs, so
local development needs no manual export:
# .env (keep this out of version control)
JWT_SECRET=change-me-in-prod
OPENAI_API_KEY=sk-...
DATABASE_URL=postgres://user:pass@localhost/app
api_key = env_get("OPENAI_API_KEY", "")
If api_key == "" Then
Display "OPENAI_API_KEY is not set"
exit_code(1)
End
Rules:
- Real environment variables always win over
.env— so the same.envworks in dev while CI / your container platform inject the real values in prod. .envis not loaded under--sandbox(andenv_getis blocked there).- Disable auto-loading entirely with
EPL_NO_DOTENV=1. - Add
.envto.gitignore; commit a.env.examplewith blank values instead.
To call an external API with the key, pass it in a headers map:
headers = dict_from_lists(["Authorization", "Content-Type"], ["Bearer " + api_key, "application/json"])
response = http_post("https://api.example.com/v1/chat", json_stringify(payload), headers)
Production requirements:
- keep secrets out of source (use env vars /
.env/ a secret manager) - require TLS at the proxy/load balancer
- set secure cookie/session policy
- rate-limit login and token endpoints
- log security events without logging passwords or tokens
Chatbot And AI Apps¶
Use the native WebApp DSL for HTTP routes and the Python bridge or AI module for model access.
Import "epl.ai" as ai
Route "/api/chat" responds with
message = request_data.get("message", "")
messages = [Map with role = "user" and content = message]
Try
reply = ai.chat(messages)
Send json Map with ok = True and reply = reply
Catch error
Send json Map with ok = False and error = "AI backend unavailable"
End
End
Production AI routes need timeouts, request limits, audit logs, and explicit fallback behavior.
WebSocket Helpers¶
server = ws_server_create(8090)
Function connected takes client_id
Say "Connected: " + client_id
End
Function received takes client_id, message
ws_broadcast(server, client_id + ": " + message)
End
ws_on_connect(server, connected)
ws_on_message(server, received)
ws_server_start(server)
Validate WebSocket behavior through your reverse proxy and load balancer before production use.
Observability¶
Create WebApp called app
Import "epl.observability" as obs
obs.attach(app)
Route "/" shows
Page "Home"
Heading "My App"
End
End
Start app on port 8000
When the observability module is available, it can register:
/_health/_ready/_metrics
Use route-level timing and structured logs for production workflows.
Deployment¶
Local generation:
Kubernetes:
epl deploy k8s myapp.epl --app-name my-service --image my-registry/my-service:1.0 --port 8000 --host my-service.example.com --tls --replicas 3
Cloud providers:
epl deploy aws myapp.epl --image my-service:latest --region us-east-1 --port 8000
epl deploy gcp myapp.epl --image my-service:latest --region us-central1 --port 8000
epl deploy azure myapp.epl --image my-service:latest --region eastus --port 8000
Generated deployment artifacts should be reviewed against your organization's runtime, secret, ingress, logging, and rollout policies.
Enterprise Web Checklist¶
- Validate all request data before use.
- Use
Send jsonfor API responses. - Avoid raw exception messages in user-facing responses.
- Keep secrets in environment variables or a secret manager.
- Use parameterized SQL for every database value.
- Review proxy trust, forwarded headers, TLS, and rate-limiting behavior.
- Test
GET,POST,HEAD,OPTIONS, and error paths for every public route. - Add health, readiness, and metrics endpoints for deployed services.
- Run smoke tests against the exact WSGI/ASGI/deployment entrypoint you ship.