diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Revision history for http-interchange
 
+## 0.2.0.0 -- 2023-08-14
+
+* Redo module structure
+* Introduce Headers type. Currently a low performance implementation.
+* Introduce Http.Types module, which reexports all types
+
 ## 0.1.0.0 -- 2023-07-25
 
 * Initial release
diff --git a/http-interchange.cabal b/http-interchange.cabal
--- a/http-interchange.cabal
+++ b/http-interchange.cabal
@@ -1,12 +1,15 @@
 cabal-version: 3.0
 name: http-interchange
-version: 0.1.0.0
+version: 0.2.0.0
 license: BSD-3-Clause
 license-file: LICENSE
 author: Andrew Martin
 maintainer: andrew.thaddeus@gmail.com
 synopsis: Types and serialization for HTTP
-description: Types and serialization for HTTP. Very minimal.
+description:
+  Types and serialization for HTTP. This includes things like request
+  line, status line, and headers. There is also a collection type
+  for headers that supports fast, case-insensitive lookup.
 copyright: 2023 Andrew Martin
 category: Data
 build-type: Simple
@@ -15,10 +18,12 @@
 library
   ghc-options: -Wall -O2
   exposed-modules:
-    Http.Message.Request
-    Http.Message.Response
+    Http.Request
+    Http.Response
     Http.Bodied
     Http.Header
+    Http.Headers
+    Http.Types
   build-depends:
     , base >=4.16.4.0 && <5
     , bytesmith >=0.3.10
@@ -26,6 +31,7 @@
     , primitive >=0.7.4
     , text >=2.0
     , bytebuild >=0.3.10
+    , contiguous >=0.6.4
   hs-source-dirs: src
   default-language: GHC2021
 
diff --git a/src/Http/Bodied.hs b/src/Http/Bodied.hs
--- a/src/Http/Bodied.hs
+++ b/src/Http/Bodied.hs
@@ -4,7 +4,10 @@
 
 import Data.Bytes.Chunks (Chunks)
 
+-- | An HTTP request or response with a body.
 data Bodied a = Bodied
   { metadata :: !a
+    -- ^ The request or response.
   , body :: !Chunks
+    -- ^ The body.
   }
diff --git a/src/Http/Headers.hs b/src/Http/Headers.hs
new file mode 100644
--- /dev/null
+++ b/src/Http/Headers.hs
@@ -0,0 +1,118 @@
+{-# language DuplicateRecordFields #-}
+{-# language DerivingStrategies #-}
+{-# language GeneralizedNewtypeDeriving #-}
+{-# language OverloadedStrings #-}
+
+-- TODO: Right now, this uses a crummy implementation. Instead, we
+-- should hash all the keys and search the hashes first to speed
+-- things up.
+module Http.Headers
+  ( -- * Types
+    Headers
+  , LookupException(..)
+    -- * Construct
+  , fromArray
+    -- * Expose
+  , toArray
+    -- * Lookup
+  , lookup
+  , lookupFirst
+  , lookupAll
+    -- * Specialized Lookup
+  , lookupContentType
+  , lookupContentLength
+  , lookupTransferEncoding
+  ) where
+
+import Prelude hiding (lookup)
+
+import Data.Text (Text)
+import Data.Primitive (SmallArray)
+import Http.Header (Header(Header))
+import Data.Foldable (foldl')
+
+import qualified Data.List as List
+import qualified Data.Primitive.Contiguous as C
+import qualified Data.Text as T
+import qualified Http.Header
+
+-- | Collection of HTTP headers. Supports case-insensitive lookup.
+-- This is intended to be used for small collections of headers.
+-- Expect degraded performance if this is used for collections of
+-- more than 128 headers.
+--
+-- This preserves the original order of the headers and the original
+-- case of the header names.
+newtype Headers = Headers (SmallArray Header)
+  deriving newtype (Show)
+
+-- | Many headers cannot appear more than once. This is part of
+-- the return type for 'lookup', and it helps us track whether the
+-- lookup failure was the result of something that might be expected
+-- (the header was @Missing@) or something that is definitely a mistake
+-- (the header was duplicated).
+data LookupException
+  = Duplicate
+  | Missing
+
+-- | Convert array of headers to a 'Headers' collection that supports
+-- efficient lookup.
+fromArray :: SmallArray Header -> Headers
+fromArray = Headers
+
+-- | Recover the original headers from from the 'Headers' collection.
+-- This is @O(1)@ and is most commonly used to fold over the headers.
+toArray :: Headers -> SmallArray Header
+toArray (Headers xs) = xs
+
+-- | Case insensitive lookup of an HTTP header. If the header is present,
+-- returns both the original header name (may differs in case from the
+-- header name searched for) and the header value. Only returns the first
+-- occurrence of the header.
+lookupFirst ::
+     Text -- header name
+  -> Headers
+  -> Maybe Header
+lookupFirst needle (Headers hdrs) =
+  List.find (\Header{name} -> caseInsensitiveEq needle name) hdrs
+
+-- | Lookup a header that should not appear more than one time and verify
+-- that it did not occur more than once. If it appears more than once
+-- (or less than once), returns a 'LookupException'.
+lookup ::
+     Text -- header name
+  -> Headers
+  -> Either LookupException Header
+lookup needle hdrs@(Headers xs) = case lookupFirst needle hdrs of
+  Nothing -> Left Missing
+  Just hdr ->
+    let count = foldl'
+          (\acc Header{name} -> if caseInsensitiveEq needle name
+            then acc + 1
+            else acc
+          ) (0 :: Int) xs
+     in if count > 1 then Left Duplicate else Right hdr
+
+-- | Lookup a header that may appear more than once. Some headers
+-- (e.g. @Set-Cookie@, @X-Forwarded-For@) are allowed to appear multiple
+-- times. This returns all the headers that matched along with their
+-- original names.
+lookupAll ::
+     Text -- header name
+  -> Headers
+  -> SmallArray Header
+lookupAll needle (Headers hdrs) =
+  C.filter (\Header{name} -> caseInsensitiveEq needle name) hdrs
+  
+-- TODO: Make this not allocate
+caseInsensitiveEq :: Text -> Text -> Bool
+caseInsensitiveEq a b = T.toLower a == T.toLower b
+
+lookupTransferEncoding :: Headers -> Either LookupException Header
+lookupTransferEncoding = lookup "transfer-encoding"
+
+lookupContentType :: Headers -> Either LookupException Header
+lookupContentType = lookup "content-type"
+
+lookupContentLength :: Headers -> Either LookupException Header
+lookupContentLength = lookup "content-length"
diff --git a/src/Http/Message/Request.hs b/src/Http/Message/Request.hs
deleted file mode 100644
--- a/src/Http/Message/Request.hs
+++ /dev/null
@@ -1,88 +0,0 @@
-{-# language LambdaCase #-}
-{-# language MagicHash #-}
-
-module Http.Message.Request
-  ( Request(..)
-  , RequestLine(..)
-    -- * Encode Request
-  , builder
-  , toChunks
-  , toChunksOnto
-    -- * Encode Request with Body
-  , bodiedToChunks
-  ) where
-
-import Data.Bytes.Builder (Builder)
-import Data.Bytes.Chunks (Chunks)
-import Data.Primitive (SmallArray)
-import Data.Text (Text)
-import Data.Word (Word8)
-import GHC.Exts (Ptr(Ptr))
-import Http.Bodied (Bodied(..))
-import Http.Header (Header)
-
-import qualified Data.Bytes.Text.Utf8 as Utf8
-import qualified Data.Bytes.Builder as Builder
-import qualified Data.Bytes.Chunks as Chunks
-import qualified Http.Header as Header
-
--- | The response status line and the response headers.
-data Request = Request
-  { requestLine :: !RequestLine
-  , headers :: !(SmallArray Header)
-  } deriving (Show)
-
-data RequestLine = RequestLine
-  { method :: {-# UNPACK #-} !Text
-  , path :: {-# UNPACK #-} !Text
-  , versionMajor :: !Word8
-  , versionMinor :: !Word8
-  } deriving (Show)
-
-builderRequestLine :: RequestLine -> Builder
-builderRequestLine RequestLine{method,path,versionMajor,versionMinor} =
-  Builder.copy (Utf8.fromText method)
-  <>
-  Builder.ascii ' '
-  <>
-  Builder.copy (Utf8.fromText path)
-  <>
-  Builder.ascii6 ' ' 'H' 'T' 'T' 'P' '/'
-  <>
-  Builder.word8Dec versionMajor
-  <>
-  Builder.ascii '.'
-  <>
-  Builder.word8Dec versionMinor
-  <>
-  Builder.ascii2 '\r' '\n'
-
-toChunks :: Request -> Chunks
-toChunks = Builder.run 256 . builder
-
-toChunksOnto :: Request -> Chunks -> Chunks
-toChunksOnto r ch = Builder.runOnto 256 (builder r) ch
-
-builder :: Request -> Builder
-builder Request{requestLine,headers} =
-  builderRequestLine requestLine
-  <>
-  Header.builderSmallArray headers
-  <>
-  Builder.ascii2 '\r' '\n'
-
--- | This adds the Content-Length header. It must not already
--- be present.
-bodiedToChunks :: Bodied Request -> Chunks
-bodiedToChunks Bodied{metadata=Request{requestLine,headers},body} =
-  Builder.runOnto 256
-    ( builderRequestLine requestLine
-      <>
-      Header.builderSmallArray headers
-      <>
-      Builder.cstring (Ptr "Content-Length: "#)
-      <>
-      Builder.word64Dec (fromIntegral (Chunks.length body))
-      <>
-      Builder.ascii4 '\r' '\n' '\r' '\n'
-    ) body
diff --git a/src/Http/Message/Response.hs b/src/Http/Message/Response.hs
deleted file mode 100644
--- a/src/Http/Message/Response.hs
+++ /dev/null
@@ -1,74 +0,0 @@
-{-# language LambdaCase #-}
-
-module Http.Message.Response
-  ( Response(..)
-  , StatusLine(..)
-  , decode
-  ) where
-
-import Control.Monad (when)
-import Data.Bytes (Bytes)
-import Data.Bytes.Parser (Parser)
-import Data.Bytes.Types (Bytes(Bytes))
-import Data.Primitive (SmallArray,ByteArray(ByteArray))
-import Data.Word (Word8,Word16)
-import Data.Text (Text)
-import Http.Header (Header)
-
-import qualified Data.Text.Internal as Text
-import qualified Data.Text.Array
-import qualified Data.Bytes.Parser as Parser
-import qualified Data.Bytes.Parser.Latin as Latin
-import qualified Http.Header as Header
-
--- | The response status line and the response headers.
-data Response = Response
-  { statusLine :: !StatusLine
-  , headers :: !(SmallArray Header)
-  } deriving (Show)
-
-data StatusLine = StatusLine
-  { versionMajor :: !Word8
-  , versionMinor :: !Word8
-  , statusCode :: !Word16
-  , statusReason :: {-# UNPACK #-} !Text
-  } deriving (Show)
-
--- | Decode the response status line and the response headers. Fails if
--- any extraneous input is present after the double CRLF sequence that
--- ends the headers.
-decode :: Int -> Bytes -> Maybe Response
-decode !n !b =
-  Parser.parseBytesMaybe (parserResponse n <* Parser.endOfInput ()) b
-
-parserResponse ::
-     Int -- ^ Maximum number of headers
-  -> Parser () s Response
-parserResponse !n = do
-  statusLine <- parserStatusLine
-  headers <- Header.parserSmallArray n
-  pure Response{statusLine,headers}
-
--- Consumes the trailing CRLF
-parserStatusLine :: Parser () s StatusLine
-parserStatusLine = do
-  Latin.char5 () 'H' 'T' 'T' 'P' '/'
-  versionMajor <- Latin.decWord8 ()
-  Latin.char () '.'
-  versionMinor <- Latin.decWord8 ()
-  Latin.char () ' '
-  statusCode <- Latin.decWord16 ()
-  when (statusCode >= 1000) (Parser.fail ())
-  Latin.char () ' '
-  -- RFC 7230: reason-phrase = *( HTAB / SP / VCHAR / obs-text )
-  statusReason <- Parser.takeWhile $ \c ->
-    (c >= 0x20 && c <= 0x7e)
-    ||
-    (c == 0x09)
-  Latin.char2 () '\r' '\n'
-  pure StatusLine{versionMajor,versionMinor,statusCode,statusReason=unsafeBytesToText statusReason}
-
-unsafeBytesToText :: Bytes -> Text
-{-# inline unsafeBytesToText #-}
-unsafeBytesToText (Bytes (ByteArray arr) off len) =
-  Text.Text (Data.Text.Array.ByteArray arr) off len
diff --git a/src/Http/Request.hs b/src/Http/Request.hs
new file mode 100644
--- /dev/null
+++ b/src/Http/Request.hs
@@ -0,0 +1,96 @@
+{-# language DuplicateRecordFields #-}
+{-# language LambdaCase #-}
+{-# language MagicHash #-}
+
+module Http.Request
+  ( Request(..)
+  , RequestLine(..)
+    -- * Encode Request
+  , builder
+  , toChunks
+  , toChunksOnto
+    -- * Encode Request with Body
+  , bodiedToChunks
+  ) where
+
+import Data.Bytes.Builder (Builder)
+import Data.Bytes.Chunks (Chunks)
+import Data.Primitive (SmallArray)
+import Data.Text (Text)
+import Data.Word (Word8)
+import GHC.Exts (Ptr(Ptr))
+import Http.Bodied (Bodied(..))
+import Http.Header (Header)
+import Http.Headers (Headers)
+
+import qualified Data.Bytes.Text.Utf8 as Utf8
+import qualified Data.Bytes.Builder as Builder
+import qualified Data.Bytes.Chunks as Chunks
+import qualified Http.Header as Header
+import qualified Http.Headers as Headers
+
+-- | The request line and the request headers.
+data Request = Request
+  { requestLine :: !RequestLine
+  , headers :: !Headers
+  } deriving (Show)
+
+-- | An HTTP request line
+data RequestLine = RequestLine
+  { method :: {-# UNPACK #-} !Text
+  , path :: {-# UNPACK #-} !Text
+  , versionMajor :: !Word8
+  , versionMinor :: !Word8
+  } deriving (Show)
+
+builderRequestLine :: RequestLine -> Builder
+builderRequestLine RequestLine{method,path,versionMajor,versionMinor} =
+  Builder.copy (Utf8.fromText method)
+  <>
+  Builder.ascii ' '
+  <>
+  Builder.copy (Utf8.fromText path)
+  <>
+  Builder.ascii6 ' ' 'H' 'T' 'T' 'P' '/'
+  <>
+  Builder.word8Dec versionMajor
+  <>
+  Builder.ascii '.'
+  <>
+  Builder.word8Dec versionMinor
+  <>
+  Builder.ascii2 '\r' '\n'
+
+toChunks :: Request -> Chunks
+toChunks = Builder.run 256 . builder
+
+toChunksOnto :: Request -> Chunks -> Chunks
+toChunksOnto r ch = Builder.runOnto 256 (builder r) ch
+
+builder :: Request -> Builder
+builder Request{requestLine,headers} =
+  builderRequestLine requestLine
+  <>
+  Header.builderSmallArray headersArray
+  <>
+  Builder.ascii2 '\r' '\n'
+  where
+  headersArray = Headers.toArray headers
+
+-- | This adds the Content-Length header. It must not already
+-- be present.
+bodiedToChunks :: Bodied Request -> Chunks
+bodiedToChunks Bodied{metadata=Request{requestLine,headers},body} =
+  Builder.runOnto 256
+    ( builderRequestLine requestLine
+      <>
+      Header.builderSmallArray headersArray
+      <>
+      Builder.cstring (Ptr "Content-Length: "#)
+      <>
+      Builder.word64Dec (fromIntegral (Chunks.length body))
+      <>
+      Builder.ascii4 '\r' '\n' '\r' '\n'
+    ) body
+  where
+  headersArray = Headers.toArray headers
diff --git a/src/Http/Response.hs b/src/Http/Response.hs
new file mode 100644
--- /dev/null
+++ b/src/Http/Response.hs
@@ -0,0 +1,78 @@
+{-# language DuplicateRecordFields #-}
+{-# language LambdaCase #-}
+
+module Http.Response
+  ( Response(..)
+  , StatusLine(..)
+  , decode
+  ) where
+
+import Control.Monad (when)
+import Data.Bytes (Bytes)
+import Data.Bytes.Parser (Parser)
+import Data.Bytes.Types (Bytes(Bytes))
+import Data.Primitive (SmallArray,ByteArray(ByteArray))
+import Data.Word (Word8,Word16)
+import Data.Text (Text)
+import Http.Header (Header)
+import Http.Headers (Headers)
+
+import qualified Data.Text.Internal as Text
+import qualified Data.Text.Array
+import qualified Data.Bytes.Parser as Parser
+import qualified Data.Bytes.Parser.Latin as Latin
+import qualified Http.Header as Header
+import qualified Http.Headers as Headers
+
+-- | The response status line and the response headers.
+data Response = Response
+  { statusLine :: !StatusLine
+  , headers :: !Headers
+  } deriving (Show)
+
+data StatusLine = StatusLine
+  { versionMajor :: !Word8
+  , versionMinor :: !Word8
+  , statusCode :: !Word16
+  , statusReason :: {-# UNPACK #-} !Text
+  } deriving (Show)
+
+-- | Decode the response status line and the response headers. Fails if
+-- any extraneous input is present after the double CRLF sequence that
+-- ends the headers.
+decode :: Int -> Bytes -> Maybe Response
+decode !n !b =
+  Parser.parseBytesMaybe (parserResponse n <* Parser.endOfInput ()) b
+
+parserResponse ::
+     Int -- ^ Maximum number of headers
+  -> Parser () s Response
+parserResponse !n = do
+  statusLine <- parserStatusLine
+  headers0 <- Header.parserSmallArray n
+  let !headers = Headers.fromArray headers0
+  pure Response{statusLine,headers}
+
+-- Consumes the trailing CRLF
+parserStatusLine :: Parser () s StatusLine
+parserStatusLine = do
+  Latin.char5 () 'H' 'T' 'T' 'P' '/'
+  versionMajor <- Latin.decWord8 ()
+  Latin.char () '.'
+  versionMinor <- Latin.decWord8 ()
+  Latin.char () ' '
+  statusCode <- Latin.decWord16 ()
+  when (statusCode >= 1000) (Parser.fail ())
+  Latin.char () ' '
+  -- RFC 7230: reason-phrase = *( HTAB / SP / VCHAR / obs-text )
+  statusReason <- Parser.takeWhile $ \c ->
+    (c >= 0x20 && c <= 0x7e)
+    ||
+    (c == 0x09)
+  Latin.char2 () '\r' '\n'
+  pure StatusLine{versionMajor,versionMinor,statusCode,statusReason=unsafeBytesToText statusReason}
+
+unsafeBytesToText :: Bytes -> Text
+{-# inline unsafeBytesToText #-}
+unsafeBytesToText (Bytes (ByteArray arr) off len) =
+  Text.Text (Data.Text.Array.ByteArray arr) off len
diff --git a/src/Http/Types.hs b/src/Http/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Http/Types.hs
@@ -0,0 +1,22 @@
+{-# language DuplicateRecordFields #-}
+
+module Http.Types
+  ( -- * Request
+    Request(..)
+  , RequestLine(..)
+    -- * Response
+  , Response(..)
+  , StatusLine(..)
+    -- * Header
+  , Headers
+  , Header(..)
+  , LookupException(..)
+    -- * Bodied
+  , Bodied(..)
+  ) where
+
+import Http.Request (Request(..),RequestLine(..))
+import Http.Response (Response(..),StatusLine(..))
+import Http.Header (Header(..))
+import Http.Headers (Headers,LookupException(..))
+import Http.Bodied (Bodied(..))
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -6,25 +6,26 @@
   ) where
 
 import Control.Monad (when)
+import Data.Aeson (FromJSON)
+import GHC.Generics (Generic)
+import Http.Types (Request,Header)
 import System.FilePath (takeBaseName, replaceExtension)
 import Test.Tasty (defaultMain, TestTree, testGroup)
 import Test.Tasty.Golden (goldenVsString, findByExtension)
+import Http.Headers (Headers)
 import Text.Show.Pretty (ppShow)
-import Http.Header (Header)
-import Data.Aeson (FromJSON)
-import GHC.Generics (Generic)
-import Http.Message.Request (Request)
 
+import qualified Data.Aeson as Aeson
 import qualified Data.ByteString.Lazy.Char8 as LBC8
-import qualified Data.Bytes.Chunks as Chunks
 import qualified Data.Bytes as Bytes
+import qualified Data.Bytes.Chunks as Chunks
+import qualified Data.List as List
 import qualified Data.Primitive as PM
 import qualified GHC.Exts as Exts
-import qualified Http.Message.Request as Request
-import qualified Http.Message.Response as Response
 import qualified Http.Header
-import qualified Data.Aeson as Aeson
-import qualified Data.List as List
+import qualified Http.Headers as Headers
+import qualified Http.Request as Request
+import qualified Http.Response as Response
 
 main :: IO ()
 main = defaultMain =<< goldenTests
@@ -36,6 +37,9 @@
 deriving anyclass instance FromJSON Header
 deriving anyclass instance FromJSON Request.RequestLine
 deriving anyclass instance FromJSON Request.Request
+
+instance FromJSON Headers where
+  parseJSON = fmap Headers.fromArray . Aeson.parseJSON
 
 goldenTests :: IO TestTree
 goldenTests = do
