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
The main type class that all controllers must implement.instance Controller PostsController where
action ShowPostAction { postId } = do
post <- fetch postId
render ShowView { post }
Type alias for IO ResponseReceived. This is the return type of controller actions.
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"]
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
Specialized version of param for Text values.let name = paramText "name"
Specialized version of param for Int values.
Specialized version of param for Bool values.
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 }
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"
Get the raw request body.action WebhookAction = do
body <- getRequestBody
processWebhook body
Get the request path (e.g., “/Users”).let path = getRequestPath
-- Returns: "/Users"
Get the request path with query string.let pathAndQuery = getRequestPathAndQuery
-- Returns: "/Users?page=2"
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
Set a response header.action DownloadAction = do
setHeader ("Content-Disposition", "attachment; filename=export.csv")
renderPlain csvData
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 }
Redirect to a URL path.redirectToPath "/admin/dashboard"
Redirect to an external URL.redirectToUrl "https://example.com"
Session Management
From IHP.Controller.Session:
Set a session variable.action LoginAction = do
-- After authentication
setSession "userId" (show user.id)
redirectTo DashboardAction
Get a session variable.action DashboardAction = do
maybeUserId <- getSession "userId"
case maybeUserId of
Just userId -> ...
Nothing -> redirectTo LoginAction
Delete a session variable.action LogoutAction = do
deleteSession "userId"
redirectTo HomeAction
File Uploads
From IHP.Controller.FileUpload:
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:
Return a 404 Not Found response.action ShowPostAction { postId } = do
maybePost <- fetchOneOrNothing postId
case maybePost of
Just post -> render ShowView { post }
Nothing -> notFound
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
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