diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 itchyny <https://github.com/itchyny>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/mackerel-client.cabal b/mackerel-client.cabal
new file mode 100644
--- /dev/null
+++ b/mackerel-client.cabal
@@ -0,0 +1,82 @@
+name:                   mackerel-client
+version:                0.0.0
+author:                 itchyny
+maintainer:             itchyny <https://github.com/itchyny>
+license:                MIT
+license-file:           LICENSE
+category:               Web
+build-type:             Simple
+cabal-version:          >=1.8
+synopsis:               An API client library for Mackerel (https://mackerel.io)
+description:            This library provides bindings to Mackerel APIs.
+                        .
+                        The official site of Mackerel: https://mackerel.io/.
+                        The reference of Mackerel API: https://mackerel.io/api-docs/.
+
+library
+  hs-source-dirs:       src
+  ghc-options:          -Wall
+  exposed-modules:      Web.Mackerel
+                      , Web.Mackerel.Types
+                      , Web.Mackerel.Types.Organization
+                      , Web.Mackerel.Types.User
+                      , Web.Mackerel.Types.Service
+                      , Web.Mackerel.Types.Role
+                      , Web.Mackerel.Types.Host
+                      , Web.Mackerel.Types.Monitor
+                      , Web.Mackerel.Types.Alert
+                      , Web.Mackerel.Types.Dashboard
+                      , Web.Mackerel.Api
+                      , Web.Mackerel.Api.Organization
+                      , Web.Mackerel.Api.User
+                      , Web.Mackerel.Api.Service
+                      , Web.Mackerel.Api.Role
+                      , Web.Mackerel.Api.Host
+                      , Web.Mackerel.Api.Monitor
+                      , Web.Mackerel.Api.Alert
+                      , Web.Mackerel.Api.Dashboard
+                      , Web.Mackerel.Client
+                      , Web.Mackerel.Config
+  other-modules:        Web.Mackerel.Internal.TH
+                      , Web.Mackerel.Internal.Api
+  build-depends:        base >= 4.7 && < 5.0
+                      , filepath
+                      , directory
+                      , http-types
+                      , http-client
+                      , http-client-tls
+                      , bytestring
+                      , parsec
+                      , aeson
+                      , htoml
+                      , text
+                      , unordered-containers
+                      , data-default
+                      , split
+
+test-suite spec
+  hs-source-dirs:       test
+  main-is:              Spec.hs
+  type:                 exitcode-stdio-1.0
+  ghc-options:          -Wall
+  other-modules:        Types.OrganizationSpec
+                      , Types.UserSpec
+                      , Types.ServiceSpec
+                      , Types.RoleSpec
+                      , Types.HostSpec
+                      , Types.MonitorSpec
+                      , Types.AlertSpec
+                      , Types.DashboardSpec
+                      , ConfigSpec
+  build-depends:        base >= 4.7 && < 5.0
+                      , mackerel-client
+                      , aeson
+                      , aeson-qq
+                      , raw-strings-qq
+                      , unordered-containers
+                      , data-default
+                      , hspec
+
+source-repository head
+  type:     git
+  location: git@github.com:itchyny/mackerel-client-hs.git
diff --git a/src/Web/Mackerel.hs b/src/Web/Mackerel.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel.hs
@@ -0,0 +1,6 @@
+module Web.Mackerel (module Mackerel) where
+
+import Web.Mackerel.Api as Mackerel
+import Web.Mackerel.Client as Mackerel
+import Web.Mackerel.Config as Mackerel
+import Web.Mackerel.Types as Mackerel
diff --git a/src/Web/Mackerel/Api.hs b/src/Web/Mackerel/Api.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Api.hs
@@ -0,0 +1,11 @@
+module Web.Mackerel.Api (module Api, ApiError, errorStatusCode, errorMessage) where
+
+import Web.Mackerel.Api.Alert as Api
+import Web.Mackerel.Api.Dashboard as Api
+import Web.Mackerel.Api.Host as Api
+import Web.Mackerel.Api.Monitor as Api
+import Web.Mackerel.Api.Organization as Api
+import Web.Mackerel.Api.Role as Api
+import Web.Mackerel.Api.Service as Api
+import Web.Mackerel.Api.User as Api
+import Web.Mackerel.Internal.Api (ApiError, errorStatusCode, errorMessage)
diff --git a/src/Web/Mackerel/Api/Alert.hs b/src/Web/Mackerel/Api/Alert.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Api/Alert.hs
@@ -0,0 +1,26 @@
+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}
+-- | Alert API.
+module Web.Mackerel.Api.Alert (listAlerts, closeAlert) where
+
+import Data.Aeson.TH (deriveJSON)
+import qualified Data.ByteString.Char8 as BS
+import qualified Data.HashMap.Lazy as HM
+import Data.Semigroup ((<>))
+import Network.HTTP.Types (StdMethod(..))
+
+import Web.Mackerel.Client
+import Web.Mackerel.Internal.Api
+import Web.Mackerel.Internal.TH
+import Web.Mackerel.Types.Alert
+
+data ListAlertsResponse = ListAlertsResponse { responseAlerts :: [Alert] }
+$(deriveJSON options ''ListAlertsResponse)
+
+listAlerts :: Client -> IO (Either ApiError [Alert])
+listAlerts client
+  = request client GET "/api/v0/alerts" [] emptyBody (createHandler responseAlerts)
+
+closeAlert :: Client -> AlertId -> String -> IO (Either ApiError Alert)
+closeAlert client (AlertId alertId') reason = do
+  let body = Just $ HM.fromList [("reason", reason) :: (String, String)]
+  request client POST ("/api/v0/alerts/" <> BS.pack alertId' <> "/close") [] body (createHandler id)
diff --git a/src/Web/Mackerel/Api/Dashboard.hs b/src/Web/Mackerel/Api/Dashboard.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Api/Dashboard.hs
@@ -0,0 +1,43 @@
+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}
+-- | Dashboard API.
+module Web.Mackerel.Api.Dashboard
+  ( listDashboards
+  , getDashboard
+  , createDashboard
+  , updateDashboard
+  , deleteDashboard
+  ) where
+
+import Data.Aeson.TH (deriveJSON)
+import qualified Data.ByteString.Char8 as BS
+import Data.Semigroup ((<>))
+import Network.HTTP.Types (StdMethod(..))
+
+import Web.Mackerel.Client
+import Web.Mackerel.Internal.Api
+import Web.Mackerel.Internal.TH
+import Web.Mackerel.Types.Dashboard
+
+data ListDashboardsResponse = ListDashboardsResponse { responseDashboards :: [Dashboard] }
+$(deriveJSON options ''ListDashboardsResponse)
+
+listDashboards :: Client -> IO (Either ApiError [Dashboard])
+listDashboards client
+  = request client GET "/api/v0/dashboards" [] emptyBody (createHandler responseDashboards)
+
+getDashboard :: Client -> DashboardId -> IO (Either ApiError Dashboard)
+getDashboard client (DashboardId dashboardId')
+  = request client GET ("/api/v0/dashboards/" <> BS.pack dashboardId') [] emptyBody (createHandler id)
+
+createDashboard :: Client -> DashboardCreate -> IO (Either ApiError Dashboard)
+createDashboard client dashboard
+  = request client POST "/api/v0/dashboards" [] (Just dashboard) (createHandler id)
+
+updateDashboard :: Client -> Dashboard -> IO (Either ApiError Dashboard)
+updateDashboard client dashboard = do
+  let (DashboardId dashboardId') = dashboardId dashboard
+  request client PUT ("/api/v0/dashboards/" <> BS.pack dashboardId') [] (Just dashboard) (createHandler id)
+
+deleteDashboard :: Client -> DashboardId -> IO (Either ApiError Dashboard)
+deleteDashboard client (DashboardId dashboardId')
+  = request client DELETE ("/api/v0/dashboards/" <> BS.pack dashboardId') [] emptyBody (createHandler id)
diff --git a/src/Web/Mackerel/Api/Host.hs b/src/Web/Mackerel/Api/Host.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Api/Host.hs
@@ -0,0 +1,90 @@
+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}
+-- | Host API.
+module Web.Mackerel.Api.Host
+  ( createHost
+  , getHost
+  , updateHost
+  , updateHostStatus
+  , updateHostRoleFullnames
+  , retireHost
+  , listHosts
+  , ListHostsParams(..)
+  , listHostMetricNames
+  ) where
+
+import Data.Aeson.TH (deriveJSON)
+import qualified Data.ByteString.Char8 as BS
+import Data.Default (Default(..))
+import qualified Data.HashMap.Lazy as HM
+import Data.Maybe (maybeToList)
+import Data.Semigroup ((<>))
+import Network.HTTP.Types (StdMethod(..))
+
+import Web.Mackerel.Client
+import Web.Mackerel.Internal.Api
+import Web.Mackerel.Internal.TH
+import Web.Mackerel.Types.Host
+
+data CreateHostResponse = CreateHostResponse { responseId :: HostId }
+$(deriveJSON options ''CreateHostResponse)
+
+createHost :: Client -> HostCreate -> IO (Either ApiError HostId)
+createHost client host
+  = request client POST "/api/v0/hosts" [] (Just host) (createHandler responseId)
+
+data GetHostResponse = GetHostResponse { responseHost :: Host }
+$(deriveJSON options ''GetHostResponse)
+
+getHost :: Client -> HostId -> IO (Either ApiError Host)
+getHost client (HostId hostId')
+  = request client GET ("/api/v0/hosts/" <> BS.pack hostId') [] emptyBody (createHandler responseHost)
+
+updateHost :: Client -> HostId -> HostCreate -> IO (Either ApiError HostId)
+updateHost client (HostId hostId') host
+  = request client PUT ("/api/v0/hosts/" <> BS.pack hostId') [] (Just host) (createHandler responseId)
+
+data SuccessResponse = SuccessResponse { responseSuccess :: Bool }
+$(deriveJSON options ''SuccessResponse)
+
+updateHostStatus :: Client -> HostId -> HostStatus -> IO (Either ApiError Bool)
+updateHostStatus client (HostId hostId') status = do
+  let body = Just $ HM.fromList [("status", status) :: (String, HostStatus)]
+  request client POST ("/api/v0/hosts/" <> BS.pack hostId' <> "/status") [] body (createHandler responseSuccess)
+
+updateHostRoleFullnames :: Client -> HostId -> [String] -> IO (Either ApiError Bool)
+updateHostRoleFullnames client (HostId hostId') roleFullnames = do
+  let body = Just $ HM.fromList [("roleFullnames", roleFullnames ) :: (String, [String])]
+  request client PUT ("/api/v0/hosts/" <> BS.pack hostId' <> "/role-fullnames") [] body (createHandler responseSuccess)
+
+retireHost :: Client -> HostId -> IO (Either ApiError Bool)
+retireHost client (HostId hostId')
+  = request client POST ("/api/v0/hosts/" <> BS.pack hostId' <> "/retire") [] emptyBody (createHandler responseSuccess)
+
+data ListHostsParams
+  = ListHostsParams {
+    listHostsParamsService :: Maybe String,
+    listHostsParamsRoles :: [String],
+    listHostsParamsName :: [String],
+    listHostsParamsStatus :: [HostStatus]
+  }
+
+instance Default ListHostsParams where
+  def = ListHostsParams Nothing [] [] [HostStatusWorking, HostStatusStandby, HostStatusMaintenance, HostStatusPoweroff]
+
+data ListHostsResponse = ListHostsResponse { responseHosts :: [Host] }
+$(deriveJSON options ''ListHostsResponse)
+
+listHosts :: Client -> ListHostsParams -> IO (Either ApiError [Host])
+listHosts client params = do
+  let query = [ ("service", Just $ BS.pack s) | s <- maybeToList (listHostsParamsService params) ] ++
+              [ ("role", Just $ BS.pack r) | r <- listHostsParamsRoles params ] ++
+              [ ("name", Just $ BS.pack r) | r <- listHostsParamsName params ] ++
+              [ ("status", Just $ BS.pack $ show s) | s <- listHostsParamsStatus params ]
+  request client GET "/api/v0/hosts" query emptyBody (createHandler responseHosts)
+
+data ListMetricNamesResponse = ListMetricNamesResponse { responseNames :: [String] }
+$(deriveJSON options ''ListMetricNamesResponse)
+
+listHostMetricNames :: Client -> HostId -> IO (Either ApiError [String])
+listHostMetricNames client (HostId hostId')
+  = request client GET ("/api/v0/hosts/" <> BS.pack hostId' <> "/metric-names") [] emptyBody (createHandler responseNames)
diff --git a/src/Web/Mackerel/Api/Monitor.hs b/src/Web/Mackerel/Api/Monitor.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Api/Monitor.hs
@@ -0,0 +1,39 @@
+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}
+-- | Monitor API.
+module Web.Mackerel.Api.Monitor
+  ( listMonitors
+  , createMonitor
+  , updateMonitor
+  , deleteMonitor
+  ) where
+
+import Data.Aeson (Value)
+import Data.Aeson.TH (deriveJSON)
+import qualified Data.ByteString.Char8 as BS
+import Data.Semigroup ((<>))
+import Network.HTTP.Types (StdMethod(..))
+
+import Web.Mackerel.Client
+import Web.Mackerel.Internal.Api
+import Web.Mackerel.Internal.TH
+import Web.Mackerel.Types.Monitor
+
+data ListMonitorsResponse = ListMonitorsResponse { responseMonitors :: [Monitor] }
+$(deriveJSON options ''ListMonitorsResponse)
+
+listMonitors :: Client -> IO (Either ApiError [Monitor])
+listMonitors client
+  = request client GET "/api/v0/monitors" [] emptyBody (createHandler responseMonitors)
+
+createMonitor :: Client -> Monitor -> IO (Either ApiError Monitor)
+createMonitor client monitor
+  = request client POST "/api/v0/monitors" [] (Just monitor) (createHandler id)
+
+updateMonitor :: Client -> Monitor -> IO (Either ApiError Monitor)
+updateMonitor client monitor = do
+  let Just (MonitorId monitorId') = monitorId monitor
+  request client PUT ("/api/v0/monitors/" <> BS.pack monitorId') [] (Just monitor) (createHandler id)
+
+deleteMonitor :: Client -> MonitorId -> IO (Either ApiError Value)
+deleteMonitor client (MonitorId monitorId')
+  = request client DELETE ("/api/v0/monitors/" <> BS.pack monitorId') [] emptyBody (createHandler id)
diff --git a/src/Web/Mackerel/Api/Organization.hs b/src/Web/Mackerel/Api/Organization.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Api/Organization.hs
@@ -0,0 +1,12 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | Organization API.
+module Web.Mackerel.Api.Organization (getOrganization) where
+
+import Network.HTTP.Types (StdMethod(..))
+
+import Web.Mackerel.Client
+import Web.Mackerel.Internal.Api
+import Web.Mackerel.Types.Organization
+
+getOrganization :: Client -> IO (Either ApiError Organization)
+getOrganization client = request client GET "/api/v0/org" [] emptyBody (createHandler id)
diff --git a/src/Web/Mackerel/Api/Role.hs b/src/Web/Mackerel/Api/Role.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Api/Role.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}
+-- | Role API.
+module Web.Mackerel.Api.Role (listRoles) where
+
+import Data.Aeson.TH (deriveJSON)
+import qualified Data.ByteString.Char8 as BS
+import Data.Semigroup ((<>))
+import Network.HTTP.Types (StdMethod(..))
+
+import Web.Mackerel.Client
+import Web.Mackerel.Internal.Api
+import Web.Mackerel.Internal.TH
+import Web.Mackerel.Types.Role
+
+data ListRolesResponse = ListRolesResponse { responseRoles :: [Role] }
+$(deriveJSON options ''ListRolesResponse)
+
+listRoles :: Client -> String -> IO (Either ApiError [Role])
+listRoles client serviceName'
+  = request client GET ("/api/v0/services/" <> BS.pack serviceName' <> "/roles") [] emptyBody (createHandler responseRoles)
diff --git a/src/Web/Mackerel/Api/Service.hs b/src/Web/Mackerel/Api/Service.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Api/Service.hs
@@ -0,0 +1,30 @@
+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}
+-- | Service API.
+module Web.Mackerel.Api.Service
+  ( listServices
+  , listServiceMetricNames
+  ) where
+
+import Data.Aeson.TH (deriveJSON)
+import qualified Data.ByteString.Char8 as BS
+import Data.Semigroup ((<>))
+import Network.HTTP.Types (StdMethod(..))
+
+import Web.Mackerel.Client
+import Web.Mackerel.Internal.Api
+import Web.Mackerel.Internal.TH
+import Web.Mackerel.Types.Service
+
+data ListServicesResponse = ListServicesResponse { responseServices :: [Service] }
+$(deriveJSON options ''ListServicesResponse)
+
+listServices :: Client -> IO (Either ApiError [Service])
+listServices client
+  = request client GET "/api/v0/services" [] emptyBody (createHandler responseServices)
+
+data ListMetricNamesResponse = ListMetricNamesResponse { responseNames :: [String] }
+$(deriveJSON options ''ListMetricNamesResponse)
+
+listServiceMetricNames :: Client -> String -> IO (Either ApiError [String])
+listServiceMetricNames client serviceName'
+  = request client GET ("/api/v0/services/" <> BS.pack serviceName' <> "/metric-names") [] emptyBody (createHandler responseNames)
diff --git a/src/Web/Mackerel/Api/User.hs b/src/Web/Mackerel/Api/User.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Api/User.hs
@@ -0,0 +1,24 @@
+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}
+-- | User API.
+module Web.Mackerel.Api.User (listUsers, deleteUser) where
+
+import Data.Aeson.TH (deriveJSON)
+import qualified Data.ByteString.Char8 as BS
+import Data.Semigroup ((<>))
+import Network.HTTP.Types (StdMethod(..))
+
+import Web.Mackerel.Client
+import Web.Mackerel.Internal.Api
+import Web.Mackerel.Internal.TH
+import Web.Mackerel.Types.User
+
+data ListUsersResponse = ListUsersResponse { responseUsers :: [User] }
+$(deriveJSON options ''ListUsersResponse)
+
+listUsers :: Client -> IO (Either ApiError [User])
+listUsers client
+  = request client GET "/api/v0/users" [] emptyBody (createHandler responseUsers)
+
+deleteUser :: Client -> UserId -> IO (Either ApiError User)
+deleteUser client (UserId userId')
+  = request client DELETE ("/api/v0/users/" <> BS.pack userId') [] emptyBody (createHandler id)
diff --git a/src/Web/Mackerel/Client.hs b/src/Web/Mackerel/Client.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Client.hs
@@ -0,0 +1,17 @@
+module Web.Mackerel.Client (Client, apiKey, apiBase, userAgent) where
+
+import Data.Default (Default(..))
+
+data Client
+  = Client {
+    apiKey :: String,
+    apiBase :: String,
+    userAgent :: String
+  } deriving (Eq, Show)
+
+instance Default Client where
+  def = Client {
+    apiKey = "",
+    apiBase = "https://mackerel.io",
+    userAgent = "mackerel-client-hs"
+  }
diff --git a/src/Web/Mackerel/Config.hs b/src/Web/Mackerel/Config.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Config.hs
@@ -0,0 +1,108 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Web.Mackerel.Config
+  ( Config(..)
+  , HostStatusConfig(..)
+  , loadConfig
+  , parseConfig
+  , loadClient
+  , mackerelRoot
+  , confFile
+  , pidFile
+  ) where
+
+import Data.Aeson
+import Data.Aeson.Types (Value(..), Result(..), typeMismatch)
+import Data.Default (Default(..))
+import Data.List (isPrefixOf, isInfixOf)
+import Data.Maybe (fromMaybe)
+import qualified Data.Text as Text
+import qualified Data.Text.IO as Text
+import System.Directory (getHomeDirectory)
+import System.FilePath ((</>))
+import System.Info (os)
+import Text.Toml (parseTomlDoc)
+
+import Web.Mackerel.Client
+import Web.Mackerel.Types.Host
+
+data Config
+  = Config {
+    configApiBase :: Maybe String,
+    configApiKey :: Maybe String,
+    configRoot :: Maybe String,
+    configPidfile :: Maybe String,
+    configRoles :: Maybe [String],
+    configVerbose :: Maybe Bool,
+    configDiagnostic :: Maybe Bool,
+    configDisplayname :: Maybe String,
+    configHostStatus :: Maybe HostStatusConfig,
+    configHttpProxy :: Maybe String
+  } deriving (Eq, Show)
+
+instance FromJSON Config where
+  parseJSON (Object o)
+    = Config <$> o .:? "apibase" <*> o .:? "apikey" <*> o .:? "root"
+             <*> o .:? "pidfile" <*> o .:? "roles" <*> o .:? "verbose"
+             <*> o .:? "diagnostic" <*> o .:? "display_name" <*> o .:? "host_status"
+             <*> o .:? "http_proxy"
+  parseJSON o = typeMismatch "Config" o
+
+instance Default Config where
+  def = Config def def def def def def def def def def
+
+data HostStatusConfig
+  = HostStatusConfig {
+    hostStatusOnStart :: Maybe HostStatus,
+    hostStatusOnStop :: Maybe HostStatus
+  } deriving (Eq, Show)
+
+instance FromJSON HostStatusConfig where
+  parseJSON (Object o)
+    = HostStatusConfig <$> o .:? "on_start" <*> o .:? "on_stop"
+  parseJSON o = typeMismatch "HostStatusConfig" o
+
+instance Default HostStatusConfig where
+  def = HostStatusConfig def def
+
+agentName :: String
+agentName = "mackerel-agent"
+
+loadConfig :: IO (Either String Config)
+loadConfig = parseConfig <$> (Text.readFile =<< confFile)
+
+parseConfig :: Text.Text -> Either String Config
+parseConfig cnt
+  = case parseTomlDoc "" cnt of
+         Left _ -> Left "toml parse error"
+         Right res -> case fromJSON $ toJSON res of
+                           Error err -> Left err
+                           Success r -> Right r
+
+loadClient :: IO (Either String Client)
+loadClient = fmap (\c -> def { apiKey = fromMaybe (apiKey def) $ configApiKey c,
+                               apiBase = fromMaybe (apiBase def) $ configApiBase c }) <$> loadConfig
+
+mackerelRoot :: IO FilePath
+mackerelRoot | isBSD || isDarwin = getHomeDirectory >>= \home -> return $ home </> "Library" </> agentName
+             | isWindows = error "Windows is not supported yet."
+             | otherwise = return "/var/lib/mackerel-agent"
+
+confFile :: IO FilePath
+confFile | isLinux = return $ "/etc" </> agentName </> agentName ++ ".conf"
+         | otherwise = mackerelRoot >>= \root -> return $ root </> agentName ++ ".conf"
+
+pidFile :: IO FilePath
+pidFile | isLinux = return "/var/run/mackerel-agent.pid"
+        | otherwise = mackerelRoot >>= \root -> return $ root </> agentName ++ ".pid"
+
+isBSD :: Bool
+isBSD = "bsd" `isInfixOf` os
+
+isDarwin :: Bool
+isDarwin = os == "darwin"
+
+isWindows :: Bool
+isWindows = "mingw" `isPrefixOf` os
+
+isLinux :: Bool
+isLinux = not isBSD && not isDarwin && not isWindows
diff --git a/src/Web/Mackerel/Internal/Api.hs b/src/Web/Mackerel/Internal/Api.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Internal/Api.hs
@@ -0,0 +1,52 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | Internal helpers for APIs.
+module Web.Mackerel.Internal.Api (request, emptyBody, createHandler, ApiError, errorStatusCode, errorMessage) where
+
+import Control.Monad ((>=>))
+import Data.Aeson (encode, decode, (.:), FromJSON, ToJSON)
+import Data.Aeson.Types (parseMaybe)
+import qualified Data.ByteString.Char8 as BS
+import qualified Data.ByteString.Lazy as LBS
+import Data.Maybe (maybe, fromMaybe)
+import Data.Semigroup ((<>))
+import Network.HTTP.Client
+import Network.HTTP.Client.TLS (tlsManagerSettings)
+import Network.HTTP.Types (StdMethod, renderStdMethod, Query, statusCode)
+
+import Web.Mackerel.Client
+
+type ResponseHandler a = Response LBS.ByteString -> Either ApiError a
+
+-- | Error information of the response of API.
+data ApiError = ApiError { errorStatusCode :: Int, errorMessage :: String } deriving (Eq, Show)
+
+-- | Request to Mackerel.
+request :: ToJSON a => Client -> StdMethod -> BS.ByteString -> Query -> Maybe a -> ResponseHandler b -> IO (Either ApiError b)
+request client method' path' query body handler = do
+  initialRequest <- setQueryString query <$> parseRequest (apiBase client)
+  handler <$> (httpLbs initialRequest {
+    method = renderStdMethod method', path = path', requestBody = RequestBodyLBS $ maybe "" encode body,
+    requestHeaders = requestHeaders initialRequest ++ [
+      ("X-Api-Key", BS.pack $ apiKey client),
+      ("User-Agent", BS.pack $ userAgent client),
+      ("Content-Type", "application/json")
+    ]
+  } =<< newManager (if secure initialRequest then tlsManagerSettings else defaultManagerSettings))
+
+-- | Empty body (to avoid type ambiguity).
+emptyBody :: Maybe ()
+emptyBody = Nothing
+
+-- | Create an api response handler.
+createHandler :: FromJSON a => (a -> b) -> ResponseHandler b
+createHandler extractor response
+  | statusCode (responseStatus response) == 200 = maybe (Left decodeError) (Right . extractor) (decode (responseBody response))
+  | otherwise = Left $ maybe decodeError getApiError $ decode (responseBody response)
+  where getApiError json = ApiError {
+          errorStatusCode = statusCode $ responseStatus response,
+          errorMessage = fromMaybe "" $ parseMaybe (((.: "error") >=> (.: "message")) <> (.: "error")) json
+        }
+        decodeError = ApiError {
+          errorStatusCode = statusCode $ responseStatus response,
+          errorMessage = show $ responseBody response
+        }
diff --git a/src/Web/Mackerel/Internal/TH.hs b/src/Web/Mackerel/Internal/TH.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Internal/TH.hs
@@ -0,0 +1,23 @@
+-- | Helpers for deriveJSON.
+module Web.Mackerel.Internal.TH (options, kebabCase, snakeCase) where
+
+import Data.Aeson.TH (Options(..), defaultOptions)
+import Data.Char (isLower, isUpper, toLower)
+import Data.List (intercalate)
+import Data.List.Split (keepDelimsL, split, whenElt)
+
+options :: Options
+options = defaultOptions {
+  fieldLabelModifier = (\(c:cs) -> toLower c : cs) . dropWhile isLower,
+  constructorTagModifier = map toLower,
+  omitNothingFields = True
+}
+
+kebabCase :: String -> String
+kebabCase = modifyCase "-"
+
+snakeCase :: String -> String
+snakeCase = modifyCase "_"
+
+modifyCase :: String -> String -> String
+modifyCase cs = intercalate cs . map (map toLower) . dropWhile null . split (keepDelimsL $ whenElt isUpper)
diff --git a/src/Web/Mackerel/Types.hs b/src/Web/Mackerel/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Types.hs
@@ -0,0 +1,10 @@
+module Web.Mackerel.Types (module Types) where
+
+import Web.Mackerel.Types.Alert as Types
+import Web.Mackerel.Types.Dashboard as Types
+import Web.Mackerel.Types.Host as Types
+import Web.Mackerel.Types.Monitor as Types
+import Web.Mackerel.Types.Organization as Types
+import Web.Mackerel.Types.Role as Types
+import Web.Mackerel.Types.Service as Types
+import Web.Mackerel.Types.User as Types
diff --git a/src/Web/Mackerel/Types/Alert.hs b/src/Web/Mackerel/Types/Alert.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Types/Alert.hs
@@ -0,0 +1,47 @@
+{-# LANGUAGE TemplateHaskell #-}
+module Web.Mackerel.Types.Alert where
+
+import Data.Aeson
+import qualified Data.Aeson as Aeson
+import Data.Aeson.TH (deriveJSON, constructorTagModifier)
+import Data.Aeson.Types (typeMismatch)
+import Data.Char (toUpper)
+import qualified Data.Text as Text
+
+import Web.Mackerel.Internal.TH
+import Web.Mackerel.Types.Host (HostId)
+import Web.Mackerel.Types.Monitor (MonitorId, MonitorType)
+
+data AlertId = AlertId String
+            deriving (Eq, Show)
+
+instance FromJSON AlertId where
+  parseJSON (Aeson.String alertId) = return $ AlertId $ Text.unpack alertId
+  parseJSON o = typeMismatch "AlertId" o
+
+instance ToJSON AlertId where
+  toJSON (AlertId alertId) = toJSON alertId
+
+data AlertStatus = AlertStatusOk
+                 | AlertStatusCritical
+                 | AlertStatusWarning
+                 | AlertStatusUnknown
+                 deriving (Eq, Show)
+
+$(deriveJSON options { constructorTagModifier = map toUpper . drop 11 } ''AlertStatus)
+
+data Alert
+  = Alert {
+    alertId :: AlertId,
+    alertStatus :: AlertStatus,
+    alertMonitorId :: Maybe MonitorId,
+    alertType :: MonitorType,
+    alertHostId :: Maybe HostId,
+    alertValue :: Maybe Double,
+    alertMessage :: Maybe String,
+    alertReason :: Maybe String,
+    alertOpenedAt :: Integer,
+    alertClosedAt :: Maybe Integer
+  } deriving (Eq, Show)
+
+$(deriveJSON options ''Alert)
diff --git a/src/Web/Mackerel/Types/Dashboard.hs b/src/Web/Mackerel/Types/Dashboard.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Types/Dashboard.hs
@@ -0,0 +1,42 @@
+{-# LANGUAGE TemplateHaskell #-}
+module Web.Mackerel.Types.Dashboard where
+
+import Data.Aeson
+import qualified Data.Aeson as Aeson
+import Data.Aeson.TH (deriveJSON, fieldLabelModifier)
+import Data.Aeson.Types (typeMismatch)
+import Data.Char (toLower)
+import qualified Data.Text as Text
+
+import Web.Mackerel.Internal.TH
+
+data DashboardId = DashboardId String
+            deriving (Eq, Show)
+
+instance FromJSON DashboardId where
+  parseJSON (Aeson.String dashboardId') = return $ DashboardId $ Text.unpack dashboardId'
+  parseJSON o = typeMismatch "DashboardId" o
+
+instance ToJSON DashboardId where
+  toJSON (DashboardId dashboardId') = toJSON dashboardId'
+
+data DashboardCreate
+  = DashboardCreate {
+    dashboardCreateTitle :: String,
+    dashboardCreateBodyMarkdown :: String,
+    dashboardCreateUrlPath :: String
+  } deriving (Eq, Show)
+
+$(deriveJSON options { fieldLabelModifier = (\(c:cs) -> toLower c : cs) . drop 15 } ''DashboardCreate)
+
+data Dashboard
+  = Dashboard {
+    dashboardId :: DashboardId,
+    dashboardTitle :: String,
+    dashboardBodyMarkdown :: String,
+    dashboardUrlPath :: String,
+    dashboardCreatedAt :: Integer,
+    dashboardUpdatedAt :: Integer
+  } deriving (Eq, Show)
+
+$(deriveJSON options ''Dashboard)
diff --git a/src/Web/Mackerel/Types/Host.hs b/src/Web/Mackerel/Types/Host.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Types/Host.hs
@@ -0,0 +1,148 @@
+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}
+module Web.Mackerel.Types.Host where
+
+import Control.Applicative ((<|>))
+import Data.Aeson
+import qualified Data.Aeson as Aeson
+import Data.Aeson.TH (deriveJSON, constructorTagModifier, fieldLabelModifier)
+import Data.Aeson.Types (Parser, typeMismatch)
+import Data.Char (toLower)
+import Data.Default (Default(..))
+import qualified Data.HashMap.Lazy as HM
+import Data.Maybe (isJust)
+import qualified Data.Text as Text
+
+import Web.Mackerel.Internal.TH
+
+data HostId = HostId String
+            deriving (Eq, Show)
+
+instance FromJSON HostId where
+  parseJSON (Aeson.String hostId) = return $ HostId $ Text.unpack hostId
+  parseJSON o = typeMismatch "HostId" o
+
+instance ToJSON HostId where
+  toJSON (HostId hostId) = toJSON hostId
+
+data HostMetaCloud
+  = HostMetaCloud {
+    metaCloudProvider :: String,
+    metaCloudMetadata :: HM.HashMap String Value
+  } deriving (Eq, Show)
+
+$(deriveJSON options { fieldLabelModifier = map toLower . drop 9 } ''HostMetaCloud)
+
+data HostMetaCpu
+  = HostMetaCpu {
+    metaCpuCacheSize :: Maybe String,
+    metaCpuCoreId :: Maybe String,
+    metaCpuCores :: Maybe String,
+    metaCpuFamily :: Maybe String,
+    metaCpuMhz :: Maybe String,
+    metaCpuModel :: Maybe String,
+    metaCpuModelName :: Maybe String,
+    metaCpuPhysicalId :: Maybe String,
+    metaCpuStepping :: Maybe String,
+    metaCpuVendorId :: Maybe String
+  } deriving (Eq, Show)
+
+instance FromJSON HostMetaCpu where
+  parseJSON (Object o)
+    = HostMetaCpu
+      <$> o .:? "cache_size" <*> o .:? "core_id" <*> o .:? "cores" <*> o .:? "family"
+      <*> (o .:? "mhz" <|> fmap show <$> (o .:? "mhz" :: Parser (Maybe Integer)))
+      <*> (o .:? "model" <|> fmap show <$> (o .:? "model" :: Parser (Maybe Integer)))
+      <*> o .:? "model_name" <*> o .:? "physical_id" <*> o .:? "stepping" <*> o .:? "vendor_id"
+  parseJSON o = typeMismatch "HostMetaCpu" o
+
+instance ToJSON HostMetaCpu where
+  toJSON (HostMetaCpu cacheSize coreId cores family mhz model modelName physicalId stepping vendorId)
+    = object [ key .= value |
+        (key, value) <- [
+          ("cache_size", cacheSize), ("core_id", coreId), ("cores", cores), ("family", family),
+          ("mhz", mhz), ("model", model), ("model_name", modelName),
+          ("physical_id", physicalId), ("stepping", stepping), ("vendor_id", vendorId)
+        ],
+        isJust value
+    ]
+
+data HostMeta
+  = HostMeta {
+    metaAgentName :: Maybe String,
+    metaAgentRevision :: Maybe String,
+    metaAgentVersion :: Maybe String,
+    metaBlockDevice :: Maybe (HM.HashMap String (HM.HashMap String String)),
+    metaCpu :: Maybe [HostMetaCpu],
+    metaFilesystem :: Maybe (HM.HashMap String (HM.HashMap String Value)),
+    metaKernel :: Maybe (HM.HashMap String String),
+    metaMemory :: Maybe (HM.HashMap String String),
+    metaCloud :: Maybe HostMetaCloud
+  } deriving (Eq, Show)
+
+instance Default HostMeta where
+  def = HostMeta def def def def def def def def def
+
+$(deriveJSON options {
+  fieldLabelModifier
+    = \xs -> let ys = drop 4 xs in if take 5 ys == "Agent" then kebabCase ys else snakeCase ys
+} ''HostMeta)
+
+data HostStatus = HostStatusWorking
+                | HostStatusStandby
+                | HostStatusMaintenance
+                | HostStatusPoweroff
+                deriving Eq
+
+instance Show HostStatus where
+  show HostStatusWorking = "working"
+  show HostStatusStandby = "standby"
+  show HostStatusMaintenance = "maintenance"
+  show HostStatusPoweroff = "poweroff"
+
+instance Read HostStatus where
+  readsPrec _ xs = [ (hs, drop (length str) xs) | (hs, str) <- pairs', take (length str) xs == str ]
+    where pairs' = [(HostStatusWorking, "working"), (HostStatusStandby, "standby"), (HostStatusMaintenance, "maintenance"), (HostStatusPoweroff, "poweroff")]
+
+$(deriveJSON options { constructorTagModifier = map toLower . drop 10 } ''HostStatus)
+
+data HostInterface
+  = HostInterface {
+    hostInterfaceName :: String,
+    hostInterfaceMacAddress :: Maybe String,
+    hostInterfaceIpv4Addresses :: Maybe [String],
+    hostInterfaceIpv6Addresses :: Maybe [String]
+  } deriving (Eq, Show)
+
+$(deriveJSON options { fieldLabelModifier = (\(c:cs) -> toLower c : cs) . drop 13 } ''HostInterface)
+
+data Host
+  = Host {
+    hostId :: HostId,
+    hostName :: String,
+    hostDisplayName :: Maybe String,
+    hostStatus :: HostStatus,
+    hostMemo :: String,
+    hostRoles :: HM.HashMap String [String],
+    hostIsRetired :: Bool,
+    hostCreatedAt :: Integer,
+    hostMeta :: HostMeta,
+    hostInterfaces :: [HostInterface]
+  } deriving (Eq, Show)
+
+$(deriveJSON options ''Host)
+
+data HostCreate
+  = HostCreate {
+    hostCreateName :: String,
+    hostCreateDisplayName :: Maybe String,
+    hostCreateCustomIdentifier :: Maybe String,
+    hostCreateMeta :: HostMeta,
+    hostCreateInterfaces :: Maybe [HostInterface],
+    hostCreateRoleFullnames :: Maybe [String],
+    hostCreateChecks :: Maybe [String]
+  } deriving (Eq, Show)
+
+instance Default HostCreate where
+  def = HostCreate def def def def def def def
+
+$(deriveJSON options { fieldLabelModifier = (\(c:cs) -> toLower c : cs) . drop 10 } ''HostCreate)
diff --git a/src/Web/Mackerel/Types/Monitor.hs b/src/Web/Mackerel/Types/Monitor.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Types/Monitor.hs
@@ -0,0 +1,129 @@
+{-# LANGUAGE TemplateHaskell #-}
+module Web.Mackerel.Types.Monitor where
+
+import Data.Aeson
+import qualified Data.Aeson as Aeson
+import Data.Aeson.TH (deriveJSON, SumEncoding(..), sumEncoding, constructorTagModifier, fieldLabelModifier)
+import Data.Aeson.Types (typeMismatch)
+import Data.Char (toLower)
+import Data.List (isSuffixOf)
+import qualified Data.Text as Text
+
+import Web.Mackerel.Internal.TH
+
+data MonitorId = MonitorId String
+               deriving (Eq, Show)
+
+instance FromJSON MonitorId where
+  parseJSON (Aeson.String monitorId') = return $ MonitorId $ Text.unpack monitorId'
+  parseJSON o = typeMismatch "MonitorId" o
+
+instance ToJSON MonitorId where
+  toJSON (MonitorId monitorId') = toJSON monitorId'
+
+data MonitorOperator = MonitorGreaterThan | MonitorLessThan
+                     deriving Eq
+
+instance Show MonitorOperator where
+  show MonitorGreaterThan = ">"
+  show MonitorLessThan = "<"
+
+instance Read MonitorOperator where
+  readsPrec _ xs = [ (op, drop 1 xs) | (op, opstr) <- [(MonitorGreaterThan, ">"), (MonitorLessThan, "<")], take 1 xs == opstr ]
+
+instance FromJSON MonitorOperator where
+  parseJSON (Aeson.String str)
+    | Text.unpack str == ">" = return MonitorGreaterThan
+    | Text.unpack str == "<" = return MonitorLessThan
+  parseJSON o = typeMismatch "MonitorOperator" o
+
+instance ToJSON MonitorOperator where
+  toJSON op = toJSON (show op)
+
+data MonitorExternalHeader
+  = MonitorExternalHeader {
+    monitorHeaderName :: String,
+    monitorHeaderValue :: String
+  } deriving (Eq, Show)
+
+$(deriveJSON options { fieldLabelModifier = map toLower . drop 13 } ''MonitorExternalHeader)
+
+data MonitorType = MonitorTypeConnectivity
+                 | MonitorTypeHost
+                 | MonitorTypeService
+                 | MonitorTypeExternal
+                 | MonitorTypeCheck
+                 | MonitorTypeExpression
+                 deriving (Eq, Show)
+
+$(deriveJSON options { constructorTagModifier = map toLower . drop 11 } ''MonitorType)
+
+data Monitor
+  = MonitorHost {
+    monitorId :: Maybe MonitorId,
+    monitorName :: String,
+    monitorDuration :: Integer,
+    monitorMetric :: String,
+    monitorOperator :: MonitorOperator,
+    monitorWarning :: Double,
+    monitorCritical :: Double,
+    monitorIsMute :: Maybe Bool,
+    monitorNotificationInterval :: Maybe Integer,
+    monitorScopes :: Maybe [String],
+    monitorExcludeScopes :: Maybe [String]
+  }
+  | MonitorConnectivity {
+    monitorId :: Maybe MonitorId,
+    monitorName :: String,
+    monitorIsMute :: Maybe Bool,
+    monitorNotificationInterval :: Maybe Integer,
+    monitorScopes :: Maybe [String],
+    monitorExcludeScopes :: Maybe [String]
+  }
+  | MonitorService {
+    monitorId :: Maybe MonitorId,
+    monitorName :: String,
+    monitorService :: String,
+    monitorDuration :: Integer,
+    monitorMetric :: String,
+    monitorOperator :: MonitorOperator,
+    monitorWarning :: Double,
+    monitorCritical :: Double,
+    monitorIsMute :: Maybe Bool,
+    monitorNotificationInterval :: Maybe Integer
+  }
+  | MonitorExternal {
+    monitorId :: Maybe MonitorId,
+    monitorName :: String,
+    monitorUrl :: String,
+    monitorServiceOption :: Maybe String,
+    monitorResponseTimeDuration :: Maybe Double,
+    monitorResponseTimeWarning :: Maybe Double,
+    monitorResponseTimeCritical :: Maybe Double,
+    monitorContainsString :: Maybe String,
+    monitorMaxCheckAttempts :: Maybe Integer,
+    monitorCertificationExpirationWarning :: Maybe Integer,
+    monitorCertificationExpirationCritical :: Maybe Integer,
+    monitorSkipCertificateVerification :: Maybe Bool,
+    monitorHeaders :: Maybe [MonitorExternalHeader],
+    monitorIsMute :: Maybe Bool,
+    monitorNotificationInterval :: Maybe Integer
+  }
+  | MonitorExpression {
+    monitorId :: Maybe MonitorId,
+    monitorName :: String,
+    monitorExpression :: String,
+    monitorOperator :: MonitorOperator,
+    monitorWarning :: Double,
+    monitorCritical :: Double,
+    monitorIsMute :: Maybe Bool,
+    monitorNotificationInterval :: Maybe Integer
+  } deriving (Eq, Show)
+
+$(deriveJSON options {
+  sumEncoding = TaggedObject "type" "type",
+  constructorTagModifier = map toLower . drop 7,
+  fieldLabelModifier = \xs ->
+    let ys = fieldLabelModifier options xs
+        in if "Option" `isSuffixOf` ys then take (length ys - 6) ys else ys
+} ''Monitor)
diff --git a/src/Web/Mackerel/Types/Organization.hs b/src/Web/Mackerel/Types/Organization.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Types/Organization.hs
@@ -0,0 +1,11 @@
+{-# LANGUAGE TemplateHaskell #-}
+module Web.Mackerel.Types.Organization where
+
+import Data.Aeson.TH (deriveJSON)
+
+import Web.Mackerel.Internal.TH
+
+data Organization = Organization { organizationName :: String }
+                  deriving (Eq, Show)
+
+$(deriveJSON options ''Organization)
diff --git a/src/Web/Mackerel/Types/Role.hs b/src/Web/Mackerel/Types/Role.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Types/Role.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE TemplateHaskell #-}
+module Web.Mackerel.Types.Role where
+
+import Data.Aeson.TH (deriveJSON)
+
+import Web.Mackerel.Internal.TH
+
+data Role
+  = Role {
+    roleName :: String,
+    roleMemo :: String
+  } deriving (Eq, Show)
+
+$(deriveJSON options ''Role)
diff --git a/src/Web/Mackerel/Types/Service.hs b/src/Web/Mackerel/Types/Service.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Types/Service.hs
@@ -0,0 +1,15 @@
+{-# LANGUAGE TemplateHaskell #-}
+module Web.Mackerel.Types.Service where
+
+import Data.Aeson.TH (deriveJSON)
+
+import Web.Mackerel.Internal.TH
+
+data Service
+  = Service {
+    serviceName :: String,
+    serviceMemo :: String,
+    serviceRoles :: [String]
+  } deriving (Eq, Show)
+
+$(deriveJSON options ''Service)
diff --git a/src/Web/Mackerel/Types/User.hs b/src/Web/Mackerel/Types/User.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Mackerel/Types/User.hs
@@ -0,0 +1,29 @@
+{-# LANGUAGE TemplateHaskell #-}
+module Web.Mackerel.Types.User where
+
+import Data.Aeson
+import qualified Data.Aeson as Aeson
+import Data.Aeson.TH (deriveJSON)
+import Data.Aeson.Types (typeMismatch)
+import qualified Data.Text as Text
+
+import Web.Mackerel.Internal.TH
+
+data UserId = UserId String
+            deriving (Eq, Show)
+
+instance FromJSON UserId where
+  parseJSON (Aeson.String alertId) = return $ UserId $ Text.unpack alertId
+  parseJSON o = typeMismatch "UserId" o
+
+instance ToJSON UserId where
+  toJSON (UserId alertId) = toJSON alertId
+
+data User
+  = User {
+    userId :: UserId,
+    userScreenName :: String,
+    userEmail :: String
+  } deriving (Eq, Show)
+
+$(deriveJSON options ''User)
diff --git a/test/ConfigSpec.hs b/test/ConfigSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/ConfigSpec.hs
@@ -0,0 +1,63 @@
+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
+module ConfigSpec where
+
+import Control.Monad (forM_)
+import Data.Default
+import Test.Hspec
+import Text.RawString.QQ
+
+import Web.Mackerel.Config
+import Web.Mackerel.Types.Host
+
+spec :: Spec
+spec = do
+
+  let config1 = def {
+        configApiKey = Just "<Mackerel-API-KEY>"
+      }
+
+  let toml1 = [r|
+        apikey = "<Mackerel-API-KEY>"
+      |]
+
+  let config2 = Config {
+        configApiBase = Just "https://mackerel.io",
+        configApiKey = Just "<Mackerel-API-KEY>",
+        configRoot = Just "/var/lib/mackerel-agent",
+        configPidfile = Just "/var/run/mackerel-agent.pid",
+        configRoles = Just ["service1:role1", "service2:role3"],
+        configVerbose = Just True,
+        configDiagnostic = Just False,
+        configDisplayname = Just "Example Host",
+        configHostStatus = Just HostStatusConfig {
+          hostStatusOnStart = Just HostStatusWorking,
+          hostStatusOnStop = Just HostStatusPoweroff
+        },
+        configHttpProxy = Just "http://localhost:8080"
+      }
+
+  let toml2 = [r|
+        apibase = "https://mackerel.io"
+        apikey = "<Mackerel-API-KEY>"
+        root = "/var/lib/mackerel-agent"
+        pidfile = "/var/run/mackerel-agent.pid"
+
+        verbose = true
+        diagnostic = false
+
+        display_name = "Example Host"
+        http_proxy = "http://localhost:8080"
+
+        roles = [ "service1:role1", "service2:role3" ]
+
+        include = "/etc/mackerel-agent/conf.d/*.conf"
+
+        [host_status]
+        on_start = "working"
+        on_stop  = "poweroff"
+      |]
+
+  describe "parseConfig" $
+    it "should parse a configuration toml" $
+      forM_ [(toml1, config1), (toml2, config2)] $ \(toml, config) ->
+        parseConfig toml `shouldBe` Right config
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/test/Types/AlertSpec.hs b/test/Types/AlertSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Types/AlertSpec.hs
@@ -0,0 +1,107 @@
+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
+module Types.AlertSpec where
+
+import Control.Monad (forM_)
+import Data.Aeson (decode, encode, Value(..))
+import Data.Aeson.QQ
+import qualified Data.HashMap.Lazy as HM
+import Test.Hspec
+
+import Web.Mackerel.Types.Alert
+import Web.Mackerel.Types.Host
+import Web.Mackerel.Types.Monitor
+
+spec :: Spec
+spec = do
+
+  let alert1 = Alert {
+        alertId = AlertId "abcde0",
+        alertStatus = AlertStatusCritical,
+        alertMonitorId = Just $ MonitorId "abcde2",
+        alertType = MonitorTypeConnectivity,
+        alertHostId = Just $ HostId "abcde1",
+        alertValue = Nothing,
+        alertMessage = Nothing,
+        alertReason = Nothing,
+        alertOpenedAt = 1483196400,
+        alertClosedAt = Nothing
+      }
+
+  let json1 = [aesonQQ|
+    {
+      "id": "abcde0",
+      "status": "CRITICAL",
+      "monitorId": "abcde2",
+      "type": "connectivity",
+      "hostId": "abcde1",
+      "openedAt": 1483196400
+    }
+  |]
+
+  let alert2 = Alert {
+        alertId = AlertId "abcde0",
+        alertStatus = AlertStatusWarning,
+        alertMonitorId = Just $ MonitorId "abcde2",
+        alertType = MonitorTypeHost,
+        alertHostId = Just $ HostId "abcde1",
+        alertValue = Just 25.0,
+        alertMessage = Nothing,
+        alertReason = Nothing,
+        alertOpenedAt = 1483196400,
+        alertClosedAt = Nothing
+      }
+
+  let json2 = [aesonQQ|
+    {
+      "id": "abcde0",
+      "status": "WARNING",
+      "monitorId": "abcde2",
+      "type": "host",
+      "hostId": "abcde1",
+      "value": 25.0,
+      "openedAt": 1483196400
+    }
+  |]
+
+  let alert3 = Alert {
+        alertId = AlertId "abcde0",
+        alertStatus = AlertStatusCritical,
+        alertMonitorId = Just $ MonitorId "abcde2",
+        alertType = MonitorTypeExternal,
+        alertHostId = Nothing,
+        alertValue = Just 121,
+        alertMessage = Just "SSL certification will expire within 696 days (2018-11-28T21:00:00+0900)",
+        alertReason = Just "Close this alert",
+        alertOpenedAt = 1483196400,
+        alertClosedAt = Just 1483282800
+      }
+
+  let json3 = [aesonQQ|
+    {
+      "id": "abcde0",
+      "status": "CRITICAL",
+      "monitorId": "abcde2",
+      "type": "external",
+      "value": 121,
+      "message": "SSL certification will expire within 696 days (2018-11-28T21:00:00+0900)",
+      "reason": "Close this alert",
+      "openedAt": 1483196400,
+      "closedAt": 1483282800
+    }
+  |]
+
+  describe "Alert FromJSON" $ do
+    it "should parse a json" $
+      forM_ [(alert1, json1), (alert2, json2), (alert3, json3)] $ \(alert, json) ->
+        decode (encode json) `shouldBe` Just alert
+
+    it "should reject an invalid json" $ do
+      decode "{}" `shouldBe` (Nothing :: Maybe Alert)
+      let (Object hm) = json1
+      forM_ ["id", "status", "type", "openedAt"] $ \key ->
+        decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe Alert)
+
+  describe "Alert ToJSON" $
+    it "should encode into a json" $
+      forM_ [(alert1, json1), (alert2, json2), (alert3, json3)] $ \(alert, json) ->
+        decode (encode alert) `shouldBe` Just json
diff --git a/test/Types/DashboardSpec.hs b/test/Types/DashboardSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Types/DashboardSpec.hs
@@ -0,0 +1,69 @@
+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
+module Types.DashboardSpec where
+
+import Control.Monad (forM_)
+import Data.Aeson (decode, encode, Value(..))
+import Data.Aeson.QQ
+import qualified Data.HashMap.Lazy as HM
+import Test.Hspec
+
+import Web.Mackerel.Types.Dashboard
+
+spec :: Spec
+spec = do
+
+  let dashboard = Dashboard {
+        dashboardId = DashboardId "abcde",
+        dashboardTitle = "This is a dashboard",
+        dashboardBodyMarkdown = "# Example\n[example](https://example.com)",
+        dashboardUrlPath = "example",
+        dashboardCreatedAt = 1483196400,
+        dashboardUpdatedAt = 1483282800
+      }
+
+  let json = [aesonQQ|
+    {
+      "id": "abcde",
+      "title": "This is a dashboard",
+      "bodyMarkdown": "# Example\n[example](https://example.com)",
+      "urlPath": "example",
+      "createdAt": 1483196400,
+      "updatedAt": 1483282800
+    }
+  |]
+
+  let dashboardCreate = DashboardCreate {
+        dashboardCreateTitle = "This is a dashboard",
+        dashboardCreateBodyMarkdown = "# Example\n[example](https://example.com)",
+        dashboardCreateUrlPath = "example"
+      }
+
+  let jsonCreate = [aesonQQ|
+    {
+      "title": "This is a dashboard",
+      "bodyMarkdown": "# Example\n[example](https://example.com)",
+      "urlPath": "example"
+    }
+  |]
+
+  describe "Dashboard FromJSON" $ do
+    it "should parse a json" $
+      decode (encode json) `shouldBe` Just dashboard
+
+    it "should reject an invalid json" $ do
+      decode "{}" `shouldBe` (Nothing :: Maybe Dashboard)
+      let (Object hm) = json
+      forM_ ["id", "title", "bodyMarkdown", "urlPath", "createdAt", "updatedAt"] $ \key ->
+        decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe Dashboard)
+
+  describe "Dashboard ToJSON" $
+    it "should encode into a json" $
+      decode (encode dashboard) `shouldBe` Just json
+
+  describe "DashboardCreate FromJSON" $
+    it "should parse a json" $
+      decode (encode jsonCreate) `shouldBe` Just dashboardCreate
+
+  describe "DashboardCreate ToJSON" $
+    it "should encode into a json" $
+      decode (encode dashboardCreate) `shouldBe` Just jsonCreate
diff --git a/test/Types/HostSpec.hs b/test/Types/HostSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Types/HostSpec.hs
@@ -0,0 +1,314 @@
+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
+module Types.HostSpec where
+
+import Control.Monad (forM_)
+import Data.Aeson (decode, encode, Value(..))
+import Data.Aeson.QQ
+import Data.Default
+import qualified Data.HashMap.Lazy as HM
+import Test.Hspec
+
+import Web.Mackerel.Types.Host
+
+spec :: Spec
+spec = do
+
+  let host1 = Host {
+        hostId = HostId "abcede",
+        hostName = "foohost.example",
+        hostDisplayName = Nothing,
+        hostStatus = HostStatusWorking,
+        hostMemo = "",
+        hostRoles = HM.fromList [],
+        hostIsRetired = False,
+        hostCreatedAt = 1483196400,
+        hostMeta = def,
+        hostInterfaces = []
+      }
+
+  let json1 = [aesonQQ|
+    {
+      "id": "abcede",
+      "name": "foohost.example",
+      "status": "working",
+      "memo": "",
+      "roles": {},
+      "isRetired": false,
+      "createdAt": 1483196400,
+      "meta": {},
+      "interfaces": []
+    }
+  |]
+
+  let host2 = Host {
+        hostId = HostId "abcede",
+        hostName = "foohost.example",
+        hostDisplayName = Just "Host Foo",
+        hostStatus = HostStatusWorking,
+        hostMemo = "host memo",
+        hostRoles = HM.fromList [("foo", ["bar", "baz"]), ("qux", ["quux"])],
+        hostIsRetired = False,
+        hostCreatedAt = 1483196400,
+        hostMeta = def {
+          metaAgentName = Just "mackerel-agent/0.37.0 (Revision 3c11c2c)",
+          metaAgentRevision = Just "3c11c2c",
+          metaAgentVersion = Just "0.37.0",
+          metaBlockDevice = Just $ HM.fromList [
+            ("xvda", HM.fromList [
+              ("removable", "0"),
+              ("size", "251658240")
+            ])
+          ],
+          metaCpu = Just [
+            HostMetaCpu {
+              metaCpuCacheSize = Just "25600 KB",
+              metaCpuCoreId = Just "0",
+              metaCpuCores = Just "1",
+              metaCpuFamily = Just "6",
+              metaCpuMhz = Just "2500.064",
+              metaCpuModel = Just "62",
+              metaCpuModelName = Just "Intel(R) Xeon(R) CPU E5-2670 v2 @ 2.50GHz",
+              metaCpuPhysicalId = Just "0",
+              metaCpuStepping = Just "4",
+              metaCpuVendorId = Just "GenuineIntel"
+            }
+          ],
+          metaFilesystem = Just $ HM.fromList [
+            ("/dev/xvda", HM.fromList [
+              ("kb_available", Number $ read "2526784"),
+              ("kb_size", Number $ read "8125880"),
+              ("kb_used", Number $ read "5163284"),
+              ("mount", "/"),
+              ("percent_used", "68%")
+            ])
+          ],
+          metaKernel = Just $ HM.fromList [
+            ("machine", "x86_64"),
+            ("name", "Linux"),
+            ("os", "GNU/Linux"),
+            ("platform_name", "Debian"),
+            ("platform_version", "8.3"),
+            ("release", "3.16.0-4-amd64"),
+            ("version", "#1 SMP Debian 3.16.7-ckt20-1+deb8u3 (2016-01-17)")
+          ],
+          metaMemory = Just $ HM.fromList [
+            ("active", "77456kB"),
+            ("buffers", "11720kB"),
+            ("cached", "83456kB"),
+            ("free", "3685992kB"),
+            ("inactive", "59368kB"),
+            ("total", "3865572kB")
+          ],
+          metaCloud = Just HostMetaCloud {
+            metaCloudProvider = "ec2",
+            metaCloudMetadata = HM.fromList [
+              ("ami-id", "ami-000"),
+              ("hostname", "ip-0-0-0-0"),
+              ("instance-id", "i-00000"),
+              ("instance-type", "m3.medium")
+            ]
+          }
+        },
+        hostInterfaces = [
+          HostInterface {
+            hostInterfaceName = "en0",
+            hostInterfaceMacAddress = Just "ff:ff:ff:ff:ff:ff",
+            hostInterfaceIpv4Addresses = Just ["192.168.100.2"],
+            hostInterfaceIpv6Addresses = Just ["2001:268:c00f:8a08::"]
+          }
+        ]
+      }
+
+  let json2 = [aesonQQ|
+    {
+      "id": "abcede",
+      "name": "foohost.example",
+      "displayName": "Host Foo",
+      "status": "working",
+      "memo": "host memo",
+      "roles": {
+        "foo": ["bar", "baz"],
+        "qux": ["quux"]
+      },
+      "isRetired": false,
+      "createdAt": 1483196400,
+      "meta": {
+        "agent-name": "mackerel-agent/0.37.0 (Revision 3c11c2c)",
+        "agent-revision": "3c11c2c",
+        "agent-version": "0.37.0",
+        "block_device": {
+          "xvda": {
+            "removable": "0",
+            "size": "251658240"
+          }
+        },
+        "cpu": [
+          {
+            "cache_size": "25600 KB",
+            "core_id": "0",
+            "cores": "1",
+            "family": "6",
+            "mhz": "2500.064",
+            "model": "62",
+            "model_name": "Intel(R) Xeon(R) CPU E5-2670 v2 @ 2.50GHz",
+            "physical_id": "0",
+            "stepping": "4",
+            "vendor_id": "GenuineIntel"
+          }
+        ],
+        "filesystem": {
+          "/dev/xvda": {
+            "kb_available": 2526784,
+            "kb_size": 8125880,
+            "kb_used": 5163284,
+            "mount": "/",
+            "percent_used": "68%"
+          }
+        },
+        "kernel": {
+          "machine": "x86_64",
+          "name": "Linux",
+          "os": "GNU/Linux",
+          "platform_name": "Debian",
+          "platform_version": "8.3",
+          "release": "3.16.0-4-amd64",
+          "version": "#1 SMP Debian 3.16.7-ckt20-1+deb8u3 (2016-01-17)"
+        },
+        "memory": {
+          "active": "77456kB",
+          "buffers": "11720kB",
+          "cached": "83456kB",
+          "free": "3685992kB",
+          "inactive": "59368kB",
+          "total": "3865572kB"
+        },
+        "cloud": {
+          "provider": "ec2",
+          "metadata": {
+            "ami-id": "ami-000",
+            "hostname": "ip-0-0-0-0",
+            "instance-id": "i-00000",
+            "instance-type": "m3.medium"
+          }
+        }
+      },
+      "interfaces": [
+        {
+          "name": "en0",
+          "macAddress": "ff:ff:ff:ff:ff:ff",
+          "ipv4Addresses": ["192.168.100.2"],
+          "ipv6Addresses": ["2001:268:c00f:8a08::"]
+        }
+      ]
+    }
+  |]
+
+  describe "Host FromJSON" $ do
+    it "should parse a json" $
+      forM_ [(json1, host1), (json2, host2)] $ \(json, host) ->
+        decode (encode json) `shouldBe` Just host
+
+    it "should reject an invalid json" $ do
+      decode "{}" `shouldBe` (Nothing :: Maybe Host)
+      let (Object hm) = json1
+      forM_ ["id", "name", "status", "memo", "roles", "isRetired", "createdAt", "meta", "interfaces"] $ \key ->
+        decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe Host)
+      forM_ ["status", "roles", "isRetired", "createdAt", "meta", "interfaces"] $ \key ->
+        decode (encode (Object (HM.insert key "invalid" hm))) `shouldBe` (Nothing :: Maybe Host)
+
+  describe "Host ToJSON" $
+    it "should encode into a json" $
+      forM_ [(json1, host1), (json2, host2)] $ \(json, host) ->
+        decode (encode host) `shouldBe` Just json
+
+  describe "HostStatus Show" $
+    it "should show properly" $ do
+      show HostStatusWorking `shouldBe` "working"
+      show HostStatusMaintenance `shouldBe` "maintenance"
+      show [HostStatusWorking, HostStatusStandby, HostStatusMaintenance, HostStatusPoweroff] `shouldBe` "[working,standby,maintenance,poweroff]"
+
+  describe "HostStatus Read" $
+    it "should read properly" $ do
+      read "working" `shouldBe` HostStatusWorking
+      read "maintenance" `shouldBe` HostStatusMaintenance
+      read "[working,standby,maintenance,poweroff]" `shouldBe` [HostStatusWorking, HostStatusStandby, HostStatusMaintenance, HostStatusPoweroff]
+
+  let hostCreate1 = HostCreate {
+        hostCreateName = "foohost.example",
+        hostCreateDisplayName = Nothing,
+        hostCreateCustomIdentifier = Nothing,
+        hostCreateMeta = def,
+        hostCreateInterfaces = Nothing,
+        hostCreateRoleFullnames = Nothing,
+        hostCreateChecks = Nothing
+      }
+
+  let jsonCreate1 = [aesonQQ|
+    {
+      "name": "foohost.example",
+      "meta": {}
+    }
+  |]
+
+  let hostCreate2 = HostCreate {
+        hostCreateName = "foohost.example",
+        hostCreateDisplayName = Just "Foo host",
+        hostCreateCustomIdentifier = Just "foo-host-identifier",
+        hostCreateMeta = def {
+          metaAgentName = Just "mackerel-agent/0.37.0 (Revision 3c11c2c)",
+          metaAgentRevision = Just "3c11c2c",
+          metaAgentVersion = Just "0.37.0"
+        },
+        hostCreateInterfaces = Just [
+          HostInterface {
+            hostInterfaceName = "en0",
+            hostInterfaceMacAddress = Just "ff:ff:ff:ff:ff:ff",
+            hostInterfaceIpv4Addresses = Just ["192.168.100.2"],
+            hostInterfaceIpv6Addresses = Just ["2001:268:c00f:8a08::"]
+          }
+        ],
+        hostCreateRoleFullnames = Just ["foo:bar", "foo:baz"],
+        hostCreateChecks = Just [
+          "plugin.checks.access_log",
+          "plugin.checks.check_cron"
+        ]
+      }
+
+  let jsonCreate2 = [aesonQQ|
+    {
+      "name": "foohost.example",
+      "displayName":  "Foo host",
+      "customIdentifier": "foo-host-identifier",
+      "meta": {
+        "agent-name": "mackerel-agent/0.37.0 (Revision 3c11c2c)",
+        "agent-revision": "3c11c2c",
+        "agent-version": "0.37.0"
+      },
+      "interfaces": [
+        {
+          "name": "en0",
+          "macAddress": "ff:ff:ff:ff:ff:ff",
+          "ipv4Addresses": ["192.168.100.2"],
+          "ipv6Addresses": ["2001:268:c00f:8a08::"]
+        }
+      ],
+      "roleFullnames": [
+        "foo:bar",
+        "foo:baz"
+      ],
+      "checks": [
+        "plugin.checks.access_log",
+        "plugin.checks.check_cron"
+      ]
+    }
+  |]
+
+  describe "HostCreate FromJSON" $
+    it "should parse a json" $
+      forM_ [(jsonCreate1, hostCreate1), (jsonCreate2, hostCreate2)] $ \(json, host) ->
+        decode (encode json) `shouldBe` Just host
+
+  describe "HostCreate ToJSON" $
+    it "should encode into a json" $
+      forM_ [(jsonCreate1, hostCreate1), (jsonCreate2, hostCreate2)] $ \(json, host) ->
+        decode (encode host) `shouldBe` Just json
diff --git a/test/Types/MonitorSpec.hs b/test/Types/MonitorSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Types/MonitorSpec.hs
@@ -0,0 +1,173 @@
+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
+module Types.MonitorSpec where
+
+import Control.Monad (forM_)
+import Data.Aeson (decode, encode)
+import Data.Aeson.QQ
+import Test.Hspec
+
+import Web.Mackerel.Types.Monitor
+
+spec :: Spec
+spec = do
+
+  let hostMonitor = MonitorHost {
+        monitorId = Just $ MonitorId "abcde1",
+        monitorName = "Monitor custom.foo.bar",
+        monitorDuration = 5,
+        monitorMetric = "custom.foo.bar",
+        monitorOperator = MonitorGreaterThan,
+        monitorWarning = 10.0,
+        monitorCritical = 20.0,
+        monitorIsMute = Just False,
+        monitorNotificationInterval = Just 30,
+        monitorScopes = Just ["service0"],
+        monitorExcludeScopes = Just ["service0:role3"]
+      }
+
+  let hostMonitorJson = [aesonQQ|
+    {
+      "type": "host",
+      "id": "abcde1",
+      "name": "Monitor custom.foo.bar",
+      "duration": 5,
+      "metric": "custom.foo.bar",
+      "operator": ">",
+      "warning": 10.0,
+      "critical": 20.0,
+      "isMute": false,
+      "notificationInterval": 30,
+      "scopes": ["service0"],
+      "excludeScopes": ["service0:role3"]
+    }
+  |]
+
+  let connectivity = MonitorConnectivity {
+        monitorId = Just $ MonitorId "abcde2",
+        monitorName = "connectivity",
+        monitorIsMute = Just False,
+        monitorNotificationInterval = Nothing,
+        monitorScopes = Nothing,
+        monitorExcludeScopes = Nothing
+      }
+
+  let connectivityJson = [aesonQQ|
+    {
+      "type": "connectivity",
+      "id": "abcde2",
+      "name": "connectivity",
+      "isMute": false
+    }
+  |]
+
+  let serviceMonitor = MonitorService {
+        monitorId = Just $ MonitorId "abcde3",
+        monitorName = "Service count",
+        monitorService = "service1",
+        monitorDuration = 5,
+        monitorMetric = "custom.service.count",
+        monitorOperator = MonitorGreaterThan,
+        monitorWarning = 100,
+        monitorCritical = 200,
+        monitorIsMute = Just False,
+        monitorNotificationInterval = Just 30
+      }
+
+  let serviceMonitorJson = [aesonQQ|
+    {
+      "type": "service",
+      "id": "abcde3",
+      "name": "Service count",
+      "service": "service1",
+      "duration": 5,
+      "metric": "custom.service.count",
+      "operator": ">",
+      "warning": 100,
+      "critical": 200,
+      "isMute": false,
+      "notificationInterval": 30
+    }
+  |]
+
+  let externalMonitor = MonitorExternal {
+        monitorId = Just $ MonitorId "abcde4",
+        monitorName = "Example external monitor",
+        monitorUrl = "https://example.com",
+        monitorServiceOption = Just "service1",
+        monitorResponseTimeDuration = Just 5,
+        monitorResponseTimeWarning = Just 3000,
+        monitorResponseTimeCritical = Just 5000,
+        monitorContainsString = Just "Example Domain",
+        monitorMaxCheckAttempts = Just 5,
+        monitorCertificationExpirationWarning = Just 1200,
+        monitorCertificationExpirationCritical = Just 60,
+        monitorSkipCertificateVerification = Just True,
+        monitorHeaders = Just [MonitorExternalHeader "Cache-Control" "no-cache"],
+        monitorIsMute = Just True,
+        monitorNotificationInterval = Just 60
+      }
+
+  let externalMonitorJson = [aesonQQ|
+    {
+      "type": "external",
+      "id": "abcde4",
+      "name": "Example external monitor",
+      "url": "https://example.com",
+      "service": "service1",
+      "responseTimeDuration": 5,
+      "responseTimeWarning": 3000,
+      "responseTimeCritical": 5000,
+      "containsString": "Example Domain",
+      "maxCheckAttempts": 5,
+      "certificationExpirationWarning": 1200,
+      "certificationExpirationCritical": 60,
+      "skipCertificateVerification": true,
+      "headers": [{ "name": "Cache-Control", "value": "no-cache" }],
+      "isMute": true,
+      "notificationInterval": 60
+    }
+  |]
+
+  let expressionMonitor = MonitorExpression {
+        monitorId = Just $ MonitorId "abcde5",
+        monitorName = "Example expression monitor",
+        monitorExpression = "min(role(\"foo:bar\", \"custom.foo.bar\"))",
+        monitorOperator = MonitorLessThan,
+        monitorWarning = 10.0,
+        monitorCritical = 20.0,
+        monitorIsMute = Just False,
+        monitorNotificationInterval = Nothing
+      }
+
+  let expressionMonitorJson = [aesonQQ|
+    {
+      "type": "expression",
+      "id": "abcde5",
+      "name": "Example expression monitor",
+      "expression": "min(role(\"foo:bar\", \"custom.foo.bar\"))",
+      "operator": "<",
+      "warning": 10.0,
+      "critical": 20.0,
+      "isMute": false
+    }
+  |]
+
+  let monitors = [
+          (hostMonitor, hostMonitorJson),
+          (connectivity, connectivityJson),
+          (serviceMonitor, serviceMonitorJson),
+          (externalMonitor, externalMonitorJson),
+          (expressionMonitor, expressionMonitorJson)
+        ]
+
+  describe "Monitor FromJSON" $
+    it "should parse jsons" $ do
+      forM_ monitors $ \(monitor, json) ->
+        decode (encode json) `shouldBe` Just monitor
+      decode (encode (map snd monitors)) `shouldBe` Just (map fst monitors)
+
+  describe "Monitor ToJSON" $
+    it "should encode into jsons" $ do
+      forM_ monitors $ \(monitor, json) ->
+        decode (encode monitor) `shouldBe` Just json
+      decode (encode (map fst monitors)) `shouldBe` Just (map snd monitors)
diff --git a/test/Types/OrganizationSpec.hs b/test/Types/OrganizationSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Types/OrganizationSpec.hs
@@ -0,0 +1,32 @@
+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
+module Types.OrganizationSpec where
+
+import Data.Aeson (decode, encode)
+import Data.Aeson.QQ
+import Test.Hspec
+
+import Web.Mackerel.Types.Organization
+
+spec :: Spec
+spec = do
+
+  let organization = Organization {
+        organizationName = "FooOrganization"
+      }
+
+  let json = [aesonQQ|
+    {
+      "name": "FooOrganization"
+    }
+  |]
+
+  describe "Organization FromJSON" $ do
+    it "should parse a json" $
+      decode (encode json) `shouldBe` Just organization
+
+    it "should reject an invalid json" $
+      decode "{}" `shouldBe` (Nothing :: Maybe Organization)
+
+  describe "Organization ToJSON" $
+    it "should encode into a json" $
+      decode (encode organization) `shouldBe` Just json
diff --git a/test/Types/RoleSpec.hs b/test/Types/RoleSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Types/RoleSpec.hs
@@ -0,0 +1,39 @@
+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
+module Types.RoleSpec where
+
+import Control.Monad (forM_)
+import Data.Aeson (decode, encode, Value(..))
+import Data.Aeson.QQ
+import qualified Data.HashMap.Lazy as HM
+import Test.Hspec
+
+import Web.Mackerel.Types.Role
+
+spec :: Spec
+spec = do
+
+  let role' = Role {
+        roleName = "FooRole",
+        roleMemo = "Role memo"
+      }
+
+  let json = [aesonQQ|
+    {
+      "name": "FooRole",
+      "memo": "Role memo"
+    }
+  |]
+
+  describe "Role FromJSON" $ do
+    it "should parse a json" $
+      decode (encode json) `shouldBe` Just role'
+
+    it "should reject an invalid json" $ do
+      decode "{}" `shouldBe` (Nothing :: Maybe Role)
+      let (Object hm) = json
+      forM_ ["name", "memo"] $ \key ->
+        decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe Role)
+
+  describe "Role ToJSON" $
+    it "should encode into a json" $
+      decode (encode role') `shouldBe` Just json
diff --git a/test/Types/ServiceSpec.hs b/test/Types/ServiceSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Types/ServiceSpec.hs
@@ -0,0 +1,45 @@
+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
+module Types.ServiceSpec where
+
+import Control.Monad (forM_)
+import Data.Aeson (decode, encode, Value(..))
+import Data.Aeson.QQ
+import qualified Data.HashMap.Lazy as HM
+import Test.Hspec
+
+import Web.Mackerel.Types.Service
+
+spec :: Spec
+spec = do
+
+  let service = Service {
+        serviceName = "FooService",
+        serviceMemo = "service memo",
+        serviceRoles = [ "role0", "role1", "role2" ]
+      }
+
+  let json = [aesonQQ|
+    {
+      "name": "FooService",
+      "memo": "service memo",
+      "roles": [
+        "role0",
+        "role1",
+        "role2"
+      ]
+    }
+  |]
+
+  describe "Service FromJSON" $ do
+    it "should parse a json" $
+      decode (encode json) `shouldBe` Just service
+
+    it "should reject an invalid json" $ do
+      decode "{}" `shouldBe` (Nothing :: Maybe Service)
+      let (Object hm) = json
+      forM_ ["name", "memo", "roles"] $ \key ->
+        decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe Service)
+
+  describe "Service ToJSON" $
+    it "should encode into a json" $
+      decode (encode service) `shouldBe` Just json
diff --git a/test/Types/UserSpec.hs b/test/Types/UserSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Types/UserSpec.hs
@@ -0,0 +1,41 @@
+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
+module Types.UserSpec where
+
+import Control.Monad (forM_)
+import Data.Aeson (decode, encode, Value(..))
+import Data.Aeson.QQ
+import qualified Data.HashMap.Lazy as HM
+import Test.Hspec
+
+import Web.Mackerel.Types.User
+
+spec :: Spec
+spec = do
+
+  let user = User {
+        userId = UserId "abcde",
+        userScreenName = "Example Mackerel",
+        userEmail = "mackerel@example.com"
+      }
+
+  let json = [aesonQQ|
+    {
+      "id": "abcde",
+      "screenName": "Example Mackerel",
+      "email": "mackerel@example.com"
+    }
+  |]
+
+  describe "User FromJSON" $ do
+    it "should parse a json" $
+      decode (encode json) `shouldBe` Just user
+
+    it "should reject an invalid json" $ do
+      decode "{}" `shouldBe` (Nothing :: Maybe User)
+      let (Object hm) = json
+      forM_ ["id", "screenName", "email"] $ \key ->
+        decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe User)
+
+  describe "User ToJSON" $
+    it "should encode into a json" $
+      decode (encode user) `shouldBe` Just json
