diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,16 @@
+# ChangeLog for wai-app-file-cgi
+
+## 3.2.0
+
+- `FileRoute`s now allow routing a requested path directly to a file
+  instead of just path prefixes to directories. For this, paths that
+  don't have a trailing path separator are interpreted as paths to
+  a file.
+
+  This change may **break** existing `FileRoute`s if they lack the
+  trailing path separator. Before upgrading, make sure that all
+  `FileRoute`s (that work correctly with `wai-app-file-cgi < 3.2`)
+  have trailing path separators in the `fileSrc` and `fileDst`
+  record fields.
+
+  See also [#23](https://github.com/kazu-yamamoto/wai-app-file-cgi/pull/23).
diff --git a/Network/Wai/Application/Classic/FileInfo.hs b/Network/Wai/Application/Classic/FileInfo.hs
--- a/Network/Wai/Application/Classic/FileInfo.hs
+++ b/Network/Wai/Application/Classic/FileInfo.hs
@@ -1,19 +1,65 @@
 module Network.Wai.Application.Classic.FileInfo where
 
+import qualified Data.ByteString as BS
 import Network.Wai
 import Network.Wai.Application.Classic.Path
 import Network.Wai.Application.Classic.Types
 
 ----------------------------------------------------------------
 
+{- $setup
+>>> :set -XOverloadedStrings
+>>> import Network.Wai (defaultRequest)
+>>> import Network.Wai.Internal (Request (..))
+>>> mkReq p = defaultRequest { rawPathInfo = p }
+>>> import Network.Wai.Application.Classic.Def (defaultFileAppSpec)
+-}
+
+{- |
+>>> pathinfoToFilePath (mkReq "/") (FileRoute "/" "/srv/http")
+"/srv/http"
+>>> pathinfoToFilePath (mkReq "/") (FileRoute "/" "/srv/http/")
+"/srv/http/"
+>>> pathinfoToFilePath (mkReq "/hello") (FileRoute "/" "/srv/http/")
+"/srv/http/hello"
+>>> pathinfoToFilePath (mkReq "/hello/") (FileRoute "/" "/srv/http/")
+"/srv/http/hello/"
+>>> pathinfoToFilePath (mkReq "/about/wai.html") (FileRoute "/" "/srv/http/")
+"/srv/http/about/wai.html"
+>>> pathinfoToFilePath (mkReq "/hello/") (FileRoute "/sub/dir/" "/var/root/")
+"/var/root/"
+>>> pathinfoToFilePath (mkReq "/sub/dir/") (FileRoute "/sub/dir/" "/var/root/")
+"/var/root/"
+>>> pathinfoToFilePath (mkReq "/sub/dir/test.html") (FileRoute "/sub/dir/" "/var/root/")
+"/var/root/test.html"
+>>> pathinfoToFilePath (mkReq "/exact/match") (FileRoute "/exact/match" "/tmp/dst")
+"/tmp/dst"
+>>> pathinfoToFilePath (mkReq "/exact/match/") (FileRoute "/exact/match" "/tmp/dst")
+"/tmp/dst/"
+>>> pathinfoToFilePath (mkReq "/exact/match/more.html") (FileRoute "/exact/match" "/tmp/dst")
+"/tmp/dst/more.html"
+-}
 pathinfoToFilePath :: Request -> FileRoute -> Path
-pathinfoToFilePath req filei = path'
+pathinfoToFilePath req filei
+  | BS.null path' = dst
+  | otherwise     = dst </> path'
   where
     path = rawPathInfo req
     src = fileSrc filei
     dst = fileDst filei
-    path' = dst </> (path <\> src) -- fixme
+    path' = path <\> src
 
+{- |
+
+It matters to 'pathinfoToFilePath' whether the 'FileRoute' has trailing slashes
+which also influences 'addIndex':
+
+>>> addIndex defaultFileAppSpec (pathinfoToFilePath (mkReq "/exact/match.html") (FileRoute "/exact/match.html" "/some/file.html"))
+"/some/file.html"
+>>> addIndex defaultFileAppSpec (pathinfoToFilePath (mkReq "/some/dir") (FileRoute "/some/dir/" "/target/dir/"))
+"/target/dir/index.html"
+
+-}
 addIndex :: FileAppSpec -> Path -> Path
 addIndex spec path
   | hasTrailingPathSeparator path = path </> indexFile spec
diff --git a/Network/Wai/Application/Classic/Types.hs b/Network/Wai/Application/Classic/Types.hs
--- a/Network/Wai/Application/Classic/Types.hs
+++ b/Network/Wai/Application/Classic/Types.hs
@@ -34,10 +34,33 @@
   , isHTML :: Path -> Bool
   }
 
+-- | 'FileRoute' describes the relationship between Paths requested via HTTP
+--   and paths to serve on disk. Specifically, it maps a /single/ path
+--   prefix to a destination path, so the 'FileRoute' needs to be applicable
+--   to the current request,
+--   e.g. @FileRoute "\/static\/" "\/var\/www\/static\/"@ will yield a 404 if
+--   @"/page.html"@ is requested.
+--   In the simplest case @FileRoute "\/" "\/var\/www\/"@ routes everything
+--   to a single destination path.
+--
+--   When resolving routes, no filesystem reads are performed, so path
+--   type needs to be inferred from the path.
+--
+--   * If 'fileSrc' and 'fileDst' have a trailing slash, source and
+--     destination are assumed to be directories (this e.g. enables
+--     fallback to index files).
+--
+--   * If 'fileSrc' and 'fileDst' lack a trailing slash, they are
+--     assumed to be files. This is supported since 3.2.0.
+--
+--   If the path type inferrable from 'FileRoute' does not match
+--   the type of the destination path on disk, routes will not
+--   resolve correctly. If the inferrable type doesn't match
+--   between 'fileSrc' and 'fileDst', routing may also misbehave.
 data FileRoute = FileRoute {
-    -- | Path prefix to be matched to 'rawPathInfo'.
+    -- | Path (prefix) to be matched to 'rawPathInfo'.
     fileSrc :: Path
-    -- | Path prefix to an actual file system.
+    -- | Path to the target directory or file.
   , fileDst :: Path
   } deriving (Eq,Show)
 
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,1 @@
+![GitHub Actions status](https://github.com/kazu-yamamoto/wai-app-file-cgi/workflows/Haskell%20CI/badge.svg)
diff --git a/test/ClassicSpec.hs b/test/ClassicSpec.hs
--- a/test/ClassicSpec.hs
+++ b/test/ClassicSpec.hs
@@ -14,12 +14,16 @@
 spec :: Spec
 spec = do
     describe "cgiApp" $ do
+        it "returns index.cgi with empty PATH_INFO for /cgi-bin/" $ do
+            let url = "http://127.0.0.1:2345/cgi-bin/"
+            bdy <- rspBody <$> sendGET url
+            bdy `shouldBe` "index.cgi\n"
         it "accepts POST" $ do
             let url = "http://127.0.0.1:2345/cgi-bin/echo-env/pathinfo?query=foo"
             bdy <- rspBody <$> sendPOST url "foo bar.\nbaz!\n"
             ans <- readFileAscii "test/data/post"
             bdy `shouldBe` ans
-        it "causes 500 if the CGI script does not exist" $ do
+        it "causes 500 if the CGI script does not behave correctly" $ do
             let url = "http://127.0.0.1:2345/cgi-bin/broken"
             sc <- rspCode <$> sendPOST url "foo bar.\nbaz!\n"
             sc `shouldBe` (5,0,0)
@@ -85,6 +89,11 @@
                 Just lm = lookupHeader HdrLocation hdr
             sc `shouldBe` (3,0,1)
             lm `shouldBe` "//127.0.0.1:2345/redirect/"
+        it "returns index.html for /exact-route.html matching exactRoute" $ do
+            let url = "http://127.0.0.1:2345/exact-route.html"
+            bdy <- rspBody <$> sendGET url
+            ans <- readFileAscii "test/html/index.html"
+            bdy `shouldBe` ans
 
 ----------------------------------------------------------------
 
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -30,14 +30,20 @@
 
 testApp :: FilePath -> Application
 testApp dir req
-    | cgi       = cgiApp  appSpec defaultCgiAppSpec  cgiRoute  req
-    | otherwise = fileApp appSpec defaultFileAppSpec fileRoute req
+    | cgi       = cgiApp  appSpec defaultCgiAppSpec  cgiRoute   req
+    | exact     = fileApp appSpec defaultFileAppSpec exactRoute req
+    | otherwise = fileApp appSpec defaultFileAppSpec fileRoute  req
   where
     cgi = "/cgi-bin/" `BS.isPrefixOf` rawPathInfo req
+    exact = "/exact-route.html" `BS.isPrefixOf` rawPathInfo req
     appSpec = defaultClassicAppSpec { softwareName = "ClassicTester" }
     cgiRoute = CgiRoute {
         cgiSrc = "/cgi-bin/"
       , cgiDst = fromString (dir </> "test/cgi-bin/")
+      }
+    exactRoute = FileRoute {
+        fileSrc = "/exact-route.html"
+      , fileDst = fromString (dir </> "test/html/index.html")
       }
     fileRoute = FileRoute {
         fileSrc = "/"
diff --git a/test/cgi-bin/index.cgi b/test/cgi-bin/index.cgi
new file mode 100644
--- /dev/null
+++ b/test/cgi-bin/index.cgi
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+echo "Content-Type: text/plain"
+echo "Status: 200"
+echo ""
+if test -n "$PATH_INFO"; then
+    echo "PATH_INFO: $PATH_INFO"
+else
+    echo "index.cgi"
+fi
diff --git a/wai-app-file-cgi.cabal b/wai-app-file-cgi.cabal
--- a/wai-app-file-cgi.cabal
+++ b/wai-app-file-cgi.cabal
@@ -1,6 +1,6 @@
-cabal-version:      >=1.10
+cabal-version:      1.18
 name:               wai-app-file-cgi
-version:            3.1.12
+version:            3.2.0
 license:            BSD3
 license-file:       LICENSE
 maintainer:         Kazu Yamamoto <kazu@iij.ad.jp>
@@ -14,7 +14,11 @@
 
 category:           Web, Yesod
 build-type:         Simple
+extra-doc-files:
+    README.md
+    CHANGELOG.md
 extra-source-files:
+    test/cgi-bin/index.cgi
     test/cgi-bin/broken
     test/cgi-bin/echo-env
     test/data/post
