phel-lang/web-skeleton

A minimalistic skeleton to build your web-app using Phel Lang.

Maintainers

Package info

github.com/phel-lang/web-skeleton

Homepage

Type:project

pkg:composer/phel-lang/web-skeleton

Statistics

Installs: 15

Dependents: 0

Suggesters: 0

Stars: 16

Open Issues: 0

dev-main 2026-06-02 14:39 UTC

This package is auto-updated.

Last update: 2026-06-02 14:39:39 UTC


README

CI

Phel is a functional Lisp that compiles to PHP. This skeleton is the fastest way to start a small Phel web app: routed HTTP server, JSON + HTML responses, middleware, request validation, a 404 handler, and tests — in a handful of .phel files.

Included: phel.router · phel.http · phel.html · phel.json · phel.schema (validation) · phel.test · error-handling middleware · CI · Docker.

Quick start

Requires PHP >= 8.4 and Composer.

composer install
composer run:dev    # http://localhost:8080 — recompiles every request

Sample routes:

Route Shows
GET / HTML page (phel.html)
GET /ping JSON response (phel.json)
POST /ping Per-method handler; echoes parsed JSON body
GET /greet/:name Path param validated with phel.schema
POST /greet JSON body parsed + schema-validated
GET /nope Custom 404 handler

Commands

composer run:dev       # dev server, recompiles every request
composer run:prod      # build once, run compiled PHP
composer build         # AOT-compile src/ into out/
composer test          # run phel tests
composer format        # format src/ and tests/
composer check         # format:check + test (run before pushing)
composer repl          # interactive Phel REPL

Project layout

src/
  app.phel               ; IO entry point (request in, response out)
  router.phel            ; route table (data) + wired handler + middleware
  config.phel            ; env-resolved configuration map
  middleware.phel        ; exception → 500, logger, server-header
  http/response.phel     ; ok/bad-request/not-found over phel.http
  controller/routes.phel ; request handlers (per method, parsing, validation)
  module/greet.phel      ; pure domain code
  module/schema.phel     ; request schemas (phel.schema, Malli-style vectors)
  view/main.phel         ; HTML view (phel.html)
tests/                   ; mirrors src/, uses phel.test
public/index.php         ; web entry — serves compiled out/ if present
phel-config.php          ; Phel build / format config

Namespaces use the dot separator (e.g. web-skeleton.controller.routes).

Routing

The route table is plain data in src/router.phel, separate from wiring so it can be inspected and tested on its own:

(def routes
  [["/" {:handler ctrl/index-handler}]
   ["/ping" {:name ::ping
             :get  {:handler ctrl/ping-get-handler}
             :post {:handler ctrl/ping-post-handler}}]
   ["/greet/{name}" {:name ::greet
                     :get  {:handler ctrl/greet-handler}}]])

Method dispatch lives in the route data (:get / :post), not the handler — the router answers 405 itself. r/handler wraps the router into a request -> response function and accepts :middleware, :not-found, :method-not-allowed, and :not-acceptable options.

r/compiled-router is faster (macro-expanded) — use it when handlers are referenced by keyword/name, since the macro embeds the route table.

Add a route: write a request -> response handler in src/controller/routes.phel, register it in routes, add a test. That's the loop.

(defn time-handler [_req] (resp/ok {:now (php/time)}))
;; ["/time" {:name ::time :get {:handler ctrl/time-handler}}]

Use the helpers in web-skeleton.http.response (resp/ok, resp/bad-request, resp/not-found) or phel.http's h/json-response / h/html-response for an explicit status.

Request validation (phel.schema)

Schemas are Malli-style vectors in module/schema.phel. Validate at the edge of a handler — sc/conform coerces and returns the value, or sc/invalid-marker on failure:

(def greet-params [:map [:name [:and :string [:re "/^.{1,50}$/"]]]])

(let [result (sc/conform greet-params {:name name})]
  (if (= result sc/invalid-marker)
    (bad-request (sc/human-readable-explain (sc/explain greet-params {:name name})))
    (ok (:name result))))

phel.schema also offers validate, coerce, generate, and instrument!.

Request bodies

phel.http decodes the body into :parsed-body — form fields for urlencoded/multipart, decoded JSON for application/json. Query string is :query-params. Handlers just read the map:

(defn greet-post-handler [req]
  (greet-response (or (:parsed-body req) {})))

:parsed-body is nil for an empty/malformed body, so (or … {}) gives a safe default and the schema reports the missing field.

Middleware

A 2-arg function (fn [handler request] ...), composed via :middleware on r/handler (global) or a route. First entry is outermost — wrap-exception goes first to catch throws and answer a JSON 500:

(defn wrap-exception [handler request]
  (try
    (handler request)
    (catch \Throwable e
      (php/error_log (str "[err] " (php/-> e (getMessage))))
      (h/json-response 500 {:error "internal server error"}))))

AI assistants

Agent-agnostic — no tool-specific files committed. Generate adapters with Phel's installer (output is .gitignored, so each dev installs their own):

vendor/bin/phel agent-install --auto    # agents already used in this project
vendor/bin/phel agent-install claude     # or: cursor, codex, gemini, copilot, aider
vendor/bin/phel agent-install --check    # catch drift after composer update

Docker

# Dev — mounts source, recompiles per request
docker compose up -d --build
docker exec -ti -u dev phel_web_skeleton bash && composer install

# Prod — multi-stage build → slim runtime serving compiled out/
docker build -f build/Dockerfile.prod -t phel-web-skeleton .
docker run --rm -p 8080:8080 phel-web-skeleton

More