Skip to main content

Overview

The IHP.ControllerSupport module provides the foundation for building controllers in IHP. It includes functionality for:
  • Reading request parameters
  • Rendering responses (HTML, JSON, plain text)
  • Session and cookie management
  • File uploads
  • Redirects and error handling
  • Authentication and authorization
This module is automatically available via IHP.ControllerPrelude in all controller files.

Core Types

Controller
Type Class
The main type class that all controllers must implement.
instance Controller PostsController where
    action ShowPostAction { postId } = do
        post <- fetch postId
        render ShowView { post }
Action'
Type Alias
Type alias for IO ResponseReceived. This is the return type of controller actions.
ControllerContext
Type
Context passed implicitly to all controller actions, containing request info, session, flash messages, etc.

Reading Parameters

From IHP.Controller.Param:
param
ParamReader valueType => ByteString -> valueType
Read a query or body parameter. Throws an exception if missing or invalid.
action UsersAction = do
    let maxItems = param @Int "maxItems"
    let email = param @Text "email"
    let userId = param @(Id User) "userId"
paramOrDefault
ParamReader a => a -> ByteString -> a
Read a parameter with a default value if missing.
action UsersAction = do
    let page = paramOrDefault @Int 0 "page"
    let limit = paramOrDefault @Int 20 "limit"
paramOrNothing
ParamReader (Maybe paramType) => ByteString -> Maybe paramType
Read a parameter that may not be present.
action SearchAction = do
    let query = paramOrNothing @Text "q"
    case query of
        Just q -> performSearch q
        Nothing -> render EmptySearchView
paramList
ParamReader valueType => ByteString -> [valueType]
Read multiple values for the same parameter name (useful for checkboxes).
action BuildFoodAction = do
    let ingredients = paramList @Text "ingredients"
    -- URL: ?ingredients=milk&ingredients=egg
    -- Result: ["milk", "egg"]
hasParam
ByteString -> Bool
Check if a parameter exists in the request.
action HelloAction = do
    if hasParam "firstname"
        then renderPlain "Hello!"
        else renderPlain "Please provide firstname"

Specialized Parameter Functions

paramText
ByteString -> Text
Specialized version of param for Text values.
let name = paramText "name"
paramInt
ByteString -> Int
Specialized version of param for Int values.
paramBool
ByteString -> Bool
Specialized version of param for Bool values.
paramUUID
ByteString -> UUID
Specialized version of param for UUID values.

Mass Assignment

fill
FillParams params record => record -> record
Fill multiple record fields from request parameters.
action UpdateUserAction { userId } = do
    user <- fetch userId
    user
        |> fill @["firstname", "lastname", "email"]
        |> updateRecord
    redirectTo UsersAction
ifValid
(Either model model -> IO r) -> model -> IO r
Branch based on validation state.
user
    |> fill @["email", "name"]
    |> validateField #email isEmail
    |> ifValid \case
        Left user -> render EditView { user }
        Right user -> do
            user <- user |> updateRecord
            redirectTo ShowUserAction { userId = user.id }

Rendering Responses

From IHP.Controller.Render:
render
View view => view -> IO ()
Render a view with the default layout. Supports content negotiation (HTML/JSON).
action ShowPostAction { postId } = do
    post <- fetch postId
    render ShowView { post }
renderPlain
LByteString -> IO ()
Render plain text response.
action HealthCheckAction = do
    renderPlain "OK"
renderJson
ToJSON json => json -> IO ()
Render JSON response.
action ApiUsersAction = do
    users <- query @User |> fetch
    renderJson users
renderFile
String -> ByteString -> IO ()
Serve a file with a content type.
action DownloadAction { fileId } = do
    file <- fetch fileId
    renderFile file.path "application/pdf"

Request Information

getRequestBody
IO LByteString
Get the raw request body.
action WebhookAction = do
    body <- getRequestBody
    processWebhook body
getRequestPath
ByteString
Get the request path (e.g., “/Users”).
let path = getRequestPath
-- Returns: "/Users"
getRequestPathAndQuery
ByteString
Get the request path with query string.
let pathAndQuery = getRequestPathAndQuery
-- Returns: "/Users?page=2"
getHeader
ByteString -> Maybe ByteString
Get a request header value (case-insensitive).
action ShowAction = do
    let contentType = getHeader "Content-Type"
    case contentType of
        Just "application/json" -> renderJson data
        _ -> render ShowView
setHeader
Header -> IO ()
Set a response header.
action DownloadAction = do
    setHeader ("Content-Disposition", "attachment; filename=export.csv")
    renderPlain csvData
request
Request
Access the current WAI Request object.
let req = request
let method = req.requestMethod

Redirects

From IHP.Controller.Redirect:
redirectTo
(?request :: Request) => action -> IO ()
Redirect to another action.
action CreatePostAction = do
    post <- newRecord @Post
        |> fill @["title", "body"]
        |> createRecord
    redirectTo ShowPostAction { postId = post.id }
redirectToPath
Text -> IO ()
Redirect to a URL path.
redirectToPath "/admin/dashboard"
redirectToUrl
Text -> IO ()
Redirect to an external URL.
redirectToUrl "https://example.com"

Session Management

From IHP.Controller.Session:
setSession
Text -> Text -> IO ()
Set a session variable.
action LoginAction = do
    -- After authentication
    setSession "userId" (show user.id)
    redirectTo DashboardAction
getSession
Text -> IO (Maybe Text)
Get a session variable.
action DashboardAction = do
    maybeUserId <- getSession "userId"
    case maybeUserId of
        Just userId -> ...
        Nothing -> redirectTo LoginAction
deleteSession
Text -> IO ()
Delete a session variable.
action LogoutAction = do
    deleteSession "userId"
    redirectTo HomeAction

File Uploads

From IHP.Controller.FileUpload:
getFiles
[File ByteString]
Get all uploaded files from the current request.
action UploadAction = do
    let files = getFiles
    forM_ files \file -> do
        storeFile file "uploads/"

Error Handling

From IHP.Controller.NotFound and IHP.Controller.AccessDenied:
notFound
IO ()
Return a 404 Not Found response.
action ShowPostAction { postId } = do
    maybePost <- fetchOneOrNothing postId
    case maybePost of
        Just post -> render ShowView { post }
        Nothing -> notFound
accessDenied
IO ()
Return a 403 Access Denied response.
action DeletePostAction { postId } = do
    post <- fetch postId
    if post.userId == currentUserId
        then deleteRecord post
        else accessDenied

Configuration

getAppConfig
Typeable configParameter => configParameter
Get a custom config parameter.
action PaymentAction = do
    let (StripePublicKey key) = getAppConfig @StripePublicKey
    render PaymentView { stripeKey = key }
First define the config in Config/Config.hs:
newtype StripePublicKey = StripePublicKey Text

config :: ConfigBuilder
config = do
    option development
    stripeKey <- StripePublicKey <$> env @Text "STRIPE_PUBLIC_KEY"
    option stripeKey

Advanced Features

beforeAction
IO ()
Hook that runs before every action in a controller.
instance Controller AdminController where
    beforeAction = do
        user <- getCurrentUser
        unless user.isAdmin $ accessDenied
    
    action DashboardAction = render DashboardView
jumpToAction
Controller action => action -> IO ()
Jump to another action without redirecting.
action CreatePostAction = do
    post <- newRecord @Post |> createRecord
    jumpToAction ShowPostAction { postId = post.id }

See Also

Build docs developers (and LLMs) love