Hackage, Stackage, and the Library Ecosystem
The Haskell ecosystem has a roughly 10,000-package central archive (Hackage) and a curated subset (Stackage). Knowing how to navigate them, judge package quality, and pick the right library for a job is what separates a productive Haskell developer from one perpetually stuck in dependency confusion.
Hackage: the package archive
Hackage is the central package repository, mirrored at hackage.haskell.org. Every package has versions, a description, dependencies, and usually links to source on GitHub. Packages follow PVP (Package Versioning Policy), which is similar to semver but stricter about API additions.
The package page tells you:
- Latest version and release date (a recent release is a good sign)
- Reverse dependencies (how many packages depend on this one)
- Build status across recent GHC versions (the matrix at the top of the page)
- License (most are BSD-3, MIT, or LGPL, check before using in commercial code)
- Documentation (Haddock-rendered)
A package with hundreds of reverse dependencies and recent activity is a safer bet than one with three dependencies last touched in 2018.
Stackage: the curated subset
Stackage takes a snapshot of Hackage versions that build together. Maintainers test the snapshot, packages with conflicts get pushed out, and the result is published as an LTS (Long Term Support) or nightly resolver.
LTS resolvers are stable for a major version (e.g., lts-22.x always uses GHC 9.6.x). New LTS major versions come out every few months as GHC versions advance. Stackage nightlies move faster.
Why this matters for library selection: a package being in Stackage is a signal that it's actively maintained enough to track the rest of the ecosystem. A package that's not in Stackage might be too new, too niche, or abandoned.
Browse Stackage at stackage.org. Each LTS resolver has a package list, version pins, and "what changed" between releases.
Finding libraries
The discovery problem in Haskell is real. There's no centralized "awesome-haskell" curated by language maintainers. Practical strategies:
- For a known need (e.g., HTTP client, JSON parser), look at what well-known projects use. Read the
cabalfiles ofpandoc,shake,xmonad, or any major project. - Search Hackage by category. Categories aren't great but help narrow.
- Use
stackage.org's package search; results are filtered to packages that build in the latest LTS. - Read recent blog posts (Tweag, IOG, Mercury, FP Complete all have engineering blogs).
- Ask in
#haskellon Libera Chat or the r/haskell subreddit. The community is small enough that questions get answered.
Judging library quality
A few signals to check before adding a dependency:
Activity. When was the last release? Last commit on the source repo? Is the maintainer responding to issues? A package abandoned for three years is risky even if it works today, because nobody will fix the next GHC compatibility issue.
Reverse dependency count. Visible on the Hackage page. If 200 packages depend on a library, it's load-bearing infrastructure. If three packages depend on it, you might be the next maintainer when something breaks.
Documentation. Haddock comments, a README with examples, ideally a tutorial. Some packages have great docs (e.g., lens, aeson); others have just type signatures. The latter requires reading source.
Dependency footprint. Packages with 50 transitive dependencies will hurt your build times and increase the chance of conflicts. The Haskell community has a slight bias toward small dependencies (the "boring Haskell" movement explicitly favors fewer, simpler dependencies), and it shows in mature libraries.
License. Mostly BSD-3 or MIT, occasionally LGPL or AGPL. For commercial code, check what the company allows. Some legal departments have opinions about LGPL.
GHC compatibility matrix. The Hackage page shows which GHC versions the latest version builds with. If your project is on GHC 9.6 and the package only supports 9.4, you have a problem.
The recommended core libraries
These are the libraries you'll see in essentially every production Haskell codebase. Knowing them well pays for itself many times over.
text: Unicode strings. Use this instead of String for anything beyond toy code. Text is a packed UTF-16 (in legacy versions) or UTF-8 representation; operations are O(1) length and proper Unicode handling.
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
main :: IO ()
main = do
let greeting = "Hello, Haskell" :: T.Text
TIO.putStrLn (T.toUpper greeting)
bytestring: Efficient byte arrays. Two flavors, strict and lazy. Use Data.ByteString for HTTP bodies, file contents, anything binary. Lazy ByteString is good for streaming large files.
containers: Map, Set, IntMap, Sequence, Tree. The standard ordered containers, all immutable, all O(log n) for most ops.
unordered-containers: HashMap and HashSet for hash-based variants. Use these when you don't need ordering, the constant factors are better.
vector: Efficient dense arrays, both boxed and unboxed. Use Data.Vector.Unboxed for arrays of primitives, the speedup over a [Int] is enormous.
aeson: JSON parsing and serialization. Standard for any HTTP API or config file:
import Data.Aeson
import GHC.Generics
data User = User { name :: String, age :: Int }
deriving (Generic, Show)
instance FromJSON User
instance ToJSON User
main :: IO ()
main = do
let json = encode (User "Alice" 30)
print json
lens: Composable getters/setters/traversals. Initially intimidating; once you internalize it, you reach for it constantly. The smaller microlens is a lighter alternative if you don't need the full power.
mtl: Monad transformer library. Reader, Writer, State, Except, plus type classes for working with them generically. Standard infrastructure for application monads.
async: Concurrency primitives, covered in the concurrency section. Use this instead of forkIO for anything more than a one-off.
time: Date and time handling. Painful but necessary. The API is more correct than most languages' time libraries; the cost is verbosity.
directory and filepath: File system operations. Cross-platform path handling.
process: Spawning subprocesses, capturing output. Standard for any CLI tool that shells out.
uuid: UUID generation and parsing.
stm: Software Transactional Memory primitives.
For HTTP:
http-client and http-client-tls: HTTP client. Lower-level than you might want; many people use req or wreq on top.
servant (covered earlier): For defining and consuming APIs with type-level descriptions.
warp: HTTP server. The de facto WSGI/Rack equivalent for Haskell. Used by Servant, Yesod, Scotty.
For databases:
postgresql-simple, persistent, beam, opaleye (covered earlier).
resource-pool: Connection pooling.
For testing:
hspec, tasty, quickcheck, hedgehog (covered in testing chapter).
For logging and observability:
co-log or katip: Structured logging. Both have learning curves; pick one and commit.
prometheus-client: Prometheus metrics.
hs-opentelemetry-sdk: OpenTelemetry, increasingly the right choice for distributed tracing.
For configuration:
envy: Environment variables to typed config.
dhall: A more sophisticated config language with types and imports. Used at Mercury and elsewhere.
yaml and aeson: For YAML/JSON config files.
A typical dependency list
For a reasonably complex API service:
build-depends:
base ^>= 4.18
, text
, bytestring
, containers
, unordered-containers
, aeson
, servant-server
, warp
, postgresql-simple
, resource-pool
, async
, stm
, mtl
, exceptions
, uuid
, time
, katip
, prometheus-client
This is a maintainable set. Most are in Stackage LTS. None are exotic. A new developer joining the team can learn this stack in a few weeks.
A flag to watch for: dependency lists with 50+ direct dependencies. This is usually a sign of accumulated dependencies that aren't all necessary, and it's worth periodic pruning.
Reading library source
A skill that pays off: when documentation isn't enough, read the source. Hackage links to source repos for most packages. Haddock can link to source per-symbol if the package was built with --haddock-hyperlink-source.
Good libraries are often the best Haskell teaching material. Reading containers, text, or aeson source is educational. Reading lens source is not (it's brilliant but dense), but reading the tutorial is.
Common Pitfalls
Adding dependencies casually. Each dependency is a maintenance burden, a build-time cost, and a potential source of GHC-incompatibility issues. Resist the urge to add a dependency for a 20-line utility you can write yourself.
Using String instead of Text. Default Haskell string literals have type String (which is [Char]). For real strings, you want Text. Enable OverloadedStrings to avoid the constant T.pack/T.unpack noise.
Mixing String, Text, and ByteString carelessly. Use Text for human-readable text, ByteString for raw bytes. Don't decode UTF-8 with T.pack . BS.unpack, use the Data.Text.Encoding functions.
Generic JSON instances on big types. deriving Generic plus instance ToJSON Foo works but the resulting JSON shape might not match what you want. Look at it before exposing externally.
Not pinning your resolver. Without a cabal.project.freeze or a Stack resolver, your build is at the mercy of Hackage's latest versions. CI will fail mysteriously when something upstream changes.
Picking trendy over boring. The effectful or polysemy effect systems are interesting but mtl is what most production code uses, and it's understood by every Haskell developer. Pick boring tools unless you have a specific reason for the trendy ones.
Key Takeaways
Hackage is the package archive; Stackage is the curated subset. Most production projects pin to a Stackage LTS or use a cabal.project.freeze for stability.
Library quality signals are activity, reverse dependency count, documentation, dependency footprint, and license. Check all of them before adding a dependency.
The recommended core (text, containers, vector, bytestring, aeson, lens, mtl, async) is what production Haskell looks like. Learn these well, you'll use them in every project.
Resist adding dependencies casually. The boring-Haskell argument for fewer dependencies is correct in the long run.