gleam-web-development
Guides Claude through Gleam backend web development with Wisp and Mist. Use for REST APIs, web services, and server-side rendering. For frontend/SPA development with Lustre, use the gleam-lustre-development skill instead.
What this skill does
# Gleam Web Development Skill
This skill guides Claude through **backend web development** with Wisp and Mist, following official examples from the Wisp repository.
## Primary Sources
1. **[Wisp Documentation](https://hexdocs.pm/wisp/)** - Practical web framework
2. **[Wisp Examples](https://github.com/lpil/wisp/tree/main/examples)** - Official examples (this skill is based on these)
3. **[Mist Documentation](https://hexdocs.pm/mist/)** - HTTP server
4. **[Gleam HTTP](https://hexdocs.pm/gleam_http/)** - HTTP types
## Project Structure
Every Wisp project follows this structure:
```
src/
├── app.gleam # Entry point
└── app/
├── router.gleam # Routing (pattern matching)
├── web.gleam # Middleware stack + Context type
└── web/
├── people.gleam # Feature module
└── products.gleam # Feature module
```
## Entry Point (app.gleam)
```gleam
import gleam/erlang/process
import mist
import wisp
import wisp_mist
import app/router
import app/web.{Context}
pub fn main() {
// Configure logger for web application defaults
wisp.configure_logger()
// In production, load from environment/config
let secret_key_base = wisp.random_string(64)
// Create context with dependencies
let ctx = Context(
db: db_connection,
static_directory: static_directory(),
)
// Partially apply context to handler
let handler = router.handle_request(_, ctx)
let assert Ok(_) =
wisp_mist.handler(handler, secret_key_base)
|> mist.new
|> mist.port(8000)
|> mist.start
process.sleep_forever()
}
pub fn static_directory() -> String {
let assert Ok(priv_directory) = wisp.priv_directory("app")
priv_directory <> "/static"
}
```
## Context Type (app/web.gleam)
The Context holds dependencies that handlers need:
```gleam
import wisp
/// Context holds dependencies for request handlers.
/// Add database connections, API keys, config, etc.
pub type Context {
Context(
db: Database,
static_directory: String,
)
}
/// The middleware stack. This is the RECOMMENDED stack for most apps.
pub fn middleware(
req: wisp.Request,
handle_request: fn(wisp.Request) -> wisp.Response,
) -> wisp.Response {
// Allow browsers to simulate PUT/DELETE via _method parameter
let req = wisp.method_override(req)
// Log request info
use <- wisp.log_request(req)
// Return 500 if handler crashes
use <- wisp.rescue_crashes
// Rewrite HEAD to GET with empty body
use req <- wisp.handle_head(req)
// CSRF protection for non-GET/HEAD requests
use req <- wisp.csrf_known_header_protection(req)
handle_request(req)
}
/// Middleware with static file serving
pub fn middleware_with_static(
req: wisp.Request,
ctx: Context,
handle_request: fn(wisp.Request) -> wisp.Response,
) -> wisp.Response {
let req = wisp.method_override(req)
use <- wisp.log_request(req)
use <- wisp.rescue_crashes
use req <- wisp.handle_head(req)
use req <- wisp.csrf_known_header_protection(req)
// Serve static files from /static/*
use <- wisp.serve_static(req, under: "/static", from: ctx.static_directory)
handle_request(req)
}
```
## Router (app/router.gleam)
Use pattern matching on `wisp.path_segments(req)`:
```gleam
import gleam/http.{Get, Post, Delete}
import wisp.{type Request, type Response}
import app/web.{type Context}
import app/web/people
import app/web/products
pub fn handle_request(req: Request, ctx: Context) -> Response {
use req <- web.middleware(req)
// Pattern match on path segments
case wisp.path_segments(req) {
// GET /
[] -> home_page(req)
// /people and /people/:id
["people"] -> people.all(req, ctx)
["people", id] -> people.one(req, ctx, id)
// /products and /products/:id
["products"] -> products.all(req, ctx)
["products", id] -> products.one(req, ctx, id)
// 404 for everything else
_ -> wisp.not_found()
}
}
fn home_page(req: Request) -> Response {
use <- wisp.require_method(req, Get)
wisp.html_response("<h1>Welcome</h1>", 200)
}
```
## Feature Modules (app/web/people.gleam)
Group related handlers in feature modules:
```gleam
import gleam/dynamic/decode
import gleam/http.{Get, Post, Delete}
import gleam/json
import gleam/result.{try}
import wisp.{type Request, type Response}
import app/web.{type Context}
// TYPES -----------------------------------------------------------------------
pub type Person {
Person(name: String, email: String)
}
// HANDLERS --------------------------------------------------------------------
/// Handle /people - list and create
pub fn all(req: Request, ctx: Context) -> Response {
case req.method {
Get -> list_people(ctx)
Post -> create_person(req, ctx)
_ -> wisp.method_not_allowed([Get, Post])
}
}
/// Handle /people/:id - read, update, delete
pub fn one(req: Request, ctx: Context, id: String) -> Response {
case req.method {
Get -> read_person(ctx, id)
Delete -> delete_person(ctx, id)
_ -> wisp.method_not_allowed([Get, Delete])
}
}
// LIST ------------------------------------------------------------------------
fn list_people(ctx: Context) -> Response {
case db.list_people(ctx.db) {
Ok(people) -> {
let json = json.to_string(json.object([
#("people", json.array(people, person_to_json)),
]))
wisp.json_response(json, 200)
}
Error(_) -> wisp.internal_server_error()
}
}
// CREATE ----------------------------------------------------------------------
fn create_person(req: Request, ctx: Context) -> Response {
// Use require_json middleware to parse JSON body
use json <- wisp.require_json(req)
let result = {
// Decode JSON into Person
use person <- try(
decode.run(json, person_decoder())
|> result.replace_error(Nil)
)
// Save to database
use id <- try(db.create_person(ctx.db, person))
// Return created response
Ok(json.to_string(json.object([#("id", json.string(id))])))
}
case result {
Ok(json) -> wisp.json_response(json, 201)
Error(_) -> wisp.unprocessable_content()
}
}
// READ ------------------------------------------------------------------------
fn read_person(ctx: Context, id: String) -> Response {
case db.find_person(ctx.db, id) {
Ok(person) -> {
let json = json.to_string(json.object([
#("id", json.string(id)),
#("name", json.string(person.name)),
#("email", json.string(person.email)),
]))
wisp.json_response(json, 200)
}
Error(_) -> wisp.not_found()
}
}
// DELETE ----------------------------------------------------------------------
fn delete_person(ctx: Context, id: String) -> Response {
case db.delete_person(ctx.db, id) {
Ok(_) -> wisp.no_content()
Error(_) -> wisp.not_found()
}
}
// DECODERS --------------------------------------------------------------------
fn person_decoder() -> decode.Decoder(Person) {
use name <- decode.field("name", decode.string)
use email <- decode.field("email", decode.string)
decode.success(Person(name:, email:))
}
fn person_to_json(person: Person) -> json.Json {
json.object([
#("name", json.string(person.name)),
#("email", json.string(person.email)),
])
}
```
## Request Body Middlewares
### JSON Body
```gleam
pub fn create_item(req: Request) -> Response {
// Parses JSON, returns 415 if wrong content-type, 400 if invalid JSON
use json <- wisp.require_json(req)
case decode.run(json, item_decoder()) {
Ok(item) -> // process item
Error(_) -> wisp.unprocessable_content()
}
}
```
### Form Data
```gleam
pub fn handle_form(req: Request) -> Response {
// Parses form data (application/x-www-form-urlencoded or multipart/form-data)
use formdata <- wisp.require_form(req)
// formdata.values is List(#(String, String))
// formdata.files is List(#(String, UploadedFile))
case list.key_find(formdata.values, "name") {
Related in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.