keinsell

Web Application in Haskell

July 31, 2026

This publication will introduce you into development process of web application with usage of Haskell programming language. I would like you to have a clue about “How does the internet work?” and “What is HTTP?” before we will proceed into intial chapter.

Iterative server loop

When you build a web application, something has to sit on a machine and wait for requests to arrive. At the lowest level that “something” is just a loop that repeatedly accepts a connection, handles it, and goes back to waiting. I’ve seen it being called an “iterative server loop” and it’s the simplest shape in which server could appear.

create socket
bind(address, port)
listen()
while true:
    conn, client_addr = accept()
    handle(conn) # then close

In everyday web development you almost always reach for a higher-level library that hides the socket machinery, and for good reason: virtually nothing above the TCP layer benefits from thinking in terms of individual connections. You don’t need a detailed understanding of how servers work with TCP sockets, nor of how the operating system manages them - knowing which abstraction you’re working at, and what you can safely ignore, is enough. The computer takes care of everything else - whether or not you understand how, and whether that turns out well or badly.

Implementing an HTTP server in Haskell means working with the abstractions the language and its ecosystem provide for talking to the lower layers of the system. In this case we need three well-established packages from Hackage: Network.Socket (to create, bind and accept TCP sockets), Network.HTTP (to parse incoming HTTP requests and send responses) and Network.URI (to extract the path from URI).

Let’s start out with “Hello World” application which would use putStrLn to display a string to you once you will run a compiled binary. I’ve assumed you know what Cabal is and compilation of application binary along execution isn’t a problem for you. We might start with .cabal package which would include dependencies.

cabal-version:      3.0
name:               web
version:            0.1.0.0
license:            BSD-3-Clause
license-file:       LICENSE
author:             keinsell
maintainer:         [email protected]
build-type:         Simple
extra-doc-files:    CHANGELOG.md

common warnings
    ghc-options: -Wall

executable haskell
    import:           warnings
    main-is:          Main.hs
    build-depends:
        base ^>=4.21.0.0,
        network ^>=3.2.8.0,
        HTTP ^>=4000.4.1,
        network-uri ^>=2.6.4.2
    hs-source-dirs:   app
    default-language: Haskell2010
module Main (main) where
main :: IO ()
main = putStrLn "Hello World"
Hello World

We would like to open a TCP socket, bind it to port 8080 on all interfaces, and start listening for connections. We will not accept any connections yet.

module Main (main) where

import Network.Socket
  ( Family(AF_INET)
  , SocketType(Stream)
  , SockAddr(SockAddrInet)
  , bind
  , defaultProtocol
  , listen
  , socket
  , tupleToHostAddress
  )

main :: IO ()
main = do
  lsock <- socket AF_INET Stream defaultProtocol
  bind lsock (SockAddrInet 8080 (tupleToHostAddress (0, 0, 0, 0)))
  listen lsock 1
  putStrLn "Listening on port 8080…"
  getLine >> pure ()   -- keep it alive until we press Enter

We have created a stream (TCP) socket for IPv4 and bound it to 0.0.0.0:8080, when we would reach our application by curl http://localhost:8080 the connection will hang because we never accepted it.

To accept connections we use forever from Control.Monad to loop indefinitely, and accept from Network.Socket to block until a client connects. When a client connects, accept returns a new socket for that connection, which we can then use to communicate with the client.

module Main (main) where

import Control.Monad (forever)
import Network.HTTP
  ( Request(rqURI)
  , Response(..)
  , close
  , receiveHTTP
  , respondHTTP
  , socketConnection
  )
import Network.Socket
  ( Family(AF_INET)
  , SocketType(Stream)
  , SockAddr(SockAddrInet)
  , accept
  , bind
  , defaultProtocol
  , listen
  , socket
  , tupleToHostAddress
  )
import Network.URI (uriPath)

main :: IO ()
main = do
  lsock <- socket AF_INET Stream defaultProtocol
  bind lsock (SockAddrInet 8080 (tupleToHostAddress (0, 0, 0, 0)))
  listen lsock 1
  putStrLn "Listening on port 8080…"

  forever $ do
    (csock, _) <- accept lsock          -- wait for a client
    hs <- socketConnection "" 8080 csock
    req <- receiveHTTP hs

    case req of
      Left _  -> close hs
      Right r -> do
        let path = uriPath (rqURI r)
            resp
              | path == "/" = Response (2,0,0) "OK"        [] "Hello, World!\n"
              | otherwise   = Response (4,0,4) "Not Found" [] "Nothing here\n"
        respondHTTP hs resp
        close hs

Application begun accepting connections, and we can now use its functionality to return a simple static greeting string when the root URL is requested, and a 404 when a request can’t be routed to the appropriate handler.

curl http://localhost:8080/          # → Hello, World!
curl http://localhost:8080/anything  # → Nothing here

There’s a long way ahead.

There’s a lot of what we didn’t touched and what have been showed up there is a drop of a water in the sea of what is left unsaid there. Before we will touch a high-level library or framework we might consider walking down at the bottom until what coming up from high-level will come to us naturally.

← Back to blog