Claude
Skills
Sign in
Back

gleam-web-development

Included with Lifetime
$97 forever

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.

Web Dev

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") {
    
Files: 1
Size: 15.0 KB
Complexity: 19/100
Category: Web Dev

Related in Web Dev