PHPackages                             phel-lang/web-skeleton - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Framework](/categories/framework)
4. /
5. phel-lang/web-skeleton

ActiveProject[Framework](/categories/framework)

phel-lang/web-skeleton
======================

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

16152PHPCI passing

Since Jun 1Pushed 1mo ago2 watchersCompare

[ Source](https://github.com/phel-lang/web-skeleton)[ Packagist](https://packagist.org/packages/phel-lang/web-skeleton)[ RSS](/packages/phel-lang-web-skeleton/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependenciesVersions (2)Used By (0)

Phel Web Skeleton
=================

[](#phel-web-skeleton)

[![CI](https://github.com/phel-lang/web-skeleton/actions/workflows/ci.yml/badge.svg)](https://github.com/phel-lang/web-skeleton/actions/workflows/ci.yml)

[Phel](https://phel-lang.org/) 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
-----------

[](#quick-start)

Requires PHP **&gt;= 8.4** and [Composer](https://getcomposer.org/).

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

Sample routes:

RouteShows`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 handlerCommands
--------

[](#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
--------------

[](#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
-------

[](#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`)
----------------------------------

[](#request-validation-phelschema)

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
--------------

[](#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
----------

[](#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
-------------

[](#ai-assistants)

Agent-agnostic — no tool-specific files committed. Generate adapters with Phel's installer (output is `.gitignore`d, 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
------

[](#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
----

[](#more)

- [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md)
- [Phel docs](https://phel-lang.org/documentation/getting-started/) · [Phel on GitHub](https://github.com/phel-lang/phel-lang)

###  Health Score

25

—

LowBetter than 35% of packages

Maintenance61

Regular maintenance activity

Popularity15

Limited adoption so far

Community11

Small or concentrated contributor base

Maturity13

Early-stage or recently created project

 Bus Factor1

Top contributor holds 95.9% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/3d166420c6770c5941e10bd68b2d26501eabb432e280d8e6eba0a344bcc1e5ae?d=identicon)[Chemaclass](/maintainers/Chemaclass)

---

Top Contributors

[![Chemaclass](https://avatars.githubusercontent.com/u/5256287?v=4)](https://github.com/Chemaclass "Chemaclass (47 commits)")[![geoffreyvanwyk](https://avatars.githubusercontent.com/u/194185?v=4)](https://github.com/geoffreyvanwyk "geoffreyvanwyk (2 commits)")

---

Tags

functional-programmingphelphel-langphpscaffoldingwebapp

### Embed Badge

![Health badge](/badges/phel-lang-web-skeleton/health.svg)

```
[![Health](https://phpackages.com/badges/phel-lang-web-skeleton/health.svg)](https://phpackages.com/packages/phel-lang-web-skeleton)
```

###  Alternatives

[laravel/dusk

Laravel Dusk provides simple end-to-end testing and browser automation.

1.9k39.6M300](/packages/laravel-dusk)[nineinchnick/edatatables

Grid widget for the Yii Framework, wrapper for the DataTables jQuery plugin

173.2k](/packages/nineinchnick-edatatables)[link-cloud/fast-hyperf

LinkCloud Fast Hyperf

241.2k1](/packages/link-cloud-fast-hyperf)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
