diff --git a/daemon/routes.go b/daemon/routes.go
index f89b0be84a3f01cef5bb15b88342dba17e61a456..3752ce85ed6f7f869dfd35ead2fe2215d726eb2b 100644
--- a/daemon/routes.go
+++ b/daemon/routes.go
@@ -24,7 +24,6 @@ func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handle
 	router := mux.NewRouter()
 	templateEngine := template.NewEngine(cfg, router)
 	apiController := api.NewController(store, feedHandler)
-	feverController := fever.NewController(cfg, store)
 	uiController := ui.NewController(cfg, store, pool, feedHandler, templateEngine, router)
 	middleware := middleware.New(cfg, store, router)
 
@@ -45,9 +44,7 @@ func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handle
 		w.Write([]byte("User-agent: *\nDisallow: /"))
 	})
 
-	feverRouter := router.PathPrefix("/fever").Subrouter()
-	feverRouter.Use(middleware.FeverAuth)
-	feverRouter.HandleFunc("/", feverController.Handler).Name("feverEndpoint")
+	fever.Serve(router, cfg, store)
 
 	apiRouter := router.PathPrefix("/v1").Subrouter()
 	apiRouter.Use(middleware.BasicAuth)
diff --git a/fever/fever.go b/fever/handler.go
similarity index 70%
rename from fever/fever.go
rename to fever/handler.go
index 4a89f45a0bcd7b4b8f64a5c52af9cf68b020e456..95d0a44b44bb16862387730ecfa27e4fd1a80a3f 100644
--- a/fever/fever.go
+++ b/fever/handler.go
@@ -1,4 +1,4 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Copyright 2018 Frédéric Guillot. All rights reserved.
 // Use of this source code is governed by the Apache 2.0
 // license that can be found in the LICENSE file.
 
@@ -17,142 +17,44 @@ import (
 	"miniflux.app/logger"
 	"miniflux.app/model"
 	"miniflux.app/storage"
-)
-
-type baseResponse struct {
-	Version       int   `json:"api_version"`
-	Authenticated int   `json:"auth"`
-	LastRefresh   int64 `json:"last_refreshed_on_time"`
-}
-
-func (b *baseResponse) SetCommonValues() {
-	b.Version = 3
-	b.Authenticated = 1
-	b.LastRefresh = time.Now().Unix()
-}
-
-/*
-The default response is a JSON object containing two members:
-
-    api_version contains the version of the API responding (positive integer)
-    auth whether the request was successfully authenticated (boolean integer)
-
-The API can also return XML by passing xml as the optional value of the api argument like so:
-
-http://yourdomain.com/fever/?api=xml
 
-The top level XML element is named response.
-
-The response to each successfully authenticated request will have auth set to 1 and include
-at least one additional member:
-
-	last_refreshed_on_time contains the time of the most recently refreshed (not updated)
-	feed (Unix timestamp/integer)
-
-*/
-func newBaseResponse() baseResponse {
-	r := baseResponse{}
-	r.SetCommonValues()
-	return r
-}
-
-type groupsResponse struct {
-	baseResponse
-	Groups      []group       `json:"groups"`
-	FeedsGroups []feedsGroups `json:"feeds_groups"`
-}
-
-type feedsResponse struct {
-	baseResponse
-	Feeds       []feed        `json:"feeds"`
-	FeedsGroups []feedsGroups `json:"feeds_groups"`
-}
-
-type faviconsResponse struct {
-	baseResponse
-	Favicons []favicon `json:"favicons"`
-}
-
-type itemsResponse struct {
-	baseResponse
-	Items []item `json:"items"`
-	Total int    `json:"total_items"`
-}
-
-type unreadResponse struct {
-	baseResponse
-	ItemIDs string `json:"unread_item_ids"`
-}
-
-type savedResponse struct {
-	baseResponse
-	ItemIDs string `json:"saved_item_ids"`
-}
-
-type group struct {
-	ID    int64  `json:"id"`
-	Title string `json:"title"`
-}
-
-type feedsGroups struct {
-	GroupID int64  `json:"group_id"`
-	FeedIDs string `json:"feed_ids"`
-}
-
-type feed struct {
-	ID          int64  `json:"id"`
-	FaviconID   int64  `json:"favicon_id"`
-	Title       string `json:"title"`
-	URL         string `json:"url"`
-	SiteURL     string `json:"site_url"`
-	IsSpark     int    `json:"is_spark"`
-	LastUpdated int64  `json:"last_updated_on_time"`
-}
+	"github.com/gorilla/mux"
+)
 
-type item struct {
-	ID        int64  `json:"id"`
-	FeedID    int64  `json:"feed_id"`
-	Title     string `json:"title"`
-	Author    string `json:"author"`
-	HTML      string `json:"html"`
-	URL       string `json:"url"`
-	IsSaved   int    `json:"is_saved"`
-	IsRead    int    `json:"is_read"`
-	CreatedAt int64  `json:"created_on_time"`
-}
+// Serve handles Fever API calls.
+func Serve(router *mux.Router, cfg *config.Config, store *storage.Storage) {
+	handler := &handler{cfg, store}
 
-type favicon struct {
-	ID   int64  `json:"id"`
-	Data string `json:"data"`
+	sr := router.PathPrefix("/fever").Subrouter()
+	sr.Use(newMiddleware(store).serve)
+	sr.HandleFunc("/", handler.serve).Name("feverEndpoint")
 }
 
-// Controller implements the Fever API.
-type Controller struct {
+type handler struct {
 	cfg   *config.Config
 	store *storage.Storage
 }
 
-// Handler handles Fever API calls
-func (c *Controller) Handler(w http.ResponseWriter, r *http.Request) {
+func (h *handler) serve(w http.ResponseWriter, r *http.Request) {
 	switch {
 	case request.HasQueryParam(r, "groups"):
-		c.handleGroups(w, r)
+		h.handleGroups(w, r)
 	case request.HasQueryParam(r, "feeds"):
-		c.handleFeeds(w, r)
+		h.handleFeeds(w, r)
 	case request.HasQueryParam(r, "favicons"):
-		c.handleFavicons(w, r)
+		h.handleFavicons(w, r)
 	case request.HasQueryParam(r, "unread_item_ids"):
-		c.handleUnreadItems(w, r)
+		h.handleUnreadItems(w, r)
 	case request.HasQueryParam(r, "saved_item_ids"):
-		c.handleSavedItems(w, r)
+		h.handleSavedItems(w, r)
 	case request.HasQueryParam(r, "items"):
-		c.handleItems(w, r)
+		h.handleItems(w, r)
 	case r.FormValue("mark") == "item":
-		c.handleWriteItems(w, r)
+		h.handleWriteItems(w, r)
 	case r.FormValue("mark") == "feed":
-		c.handleWriteFeeds(w, r)
+		h.handleWriteFeeds(w, r)
 	case r.FormValue("mark") == "group":
-		c.handleWriteGroups(w, r)
+		h.handleWriteGroups(w, r)
 	default:
 		json.OK(w, r, newBaseResponse())
 	}
@@ -178,17 +80,17 @@ The “Sparks” super group is not included in this response and is composed of
 is_spark equal to 1.
 
 */
-func (c *Controller) handleGroups(w http.ResponseWriter, r *http.Request) {
+func (h *handler) handleGroups(w http.ResponseWriter, r *http.Request) {
 	userID := request.UserID(r)
 	logger.Debug("[Fever] Fetching groups for userID=%d", userID)
 
-	categories, err := c.store.Categories(userID)
+	categories, err := h.store.Categories(userID)
 	if err != nil {
 		json.ServerError(w, r, err)
 		return
 	}
 
-	feeds, err := c.store.Feeds(userID)
+	feeds, err := h.store.Feeds(userID)
 	if err != nil {
 		json.ServerError(w, r, err)
 		return
@@ -199,7 +101,7 @@ func (c *Controller) handleGroups(w http.ResponseWriter, r *http.Request) {
 		result.Groups = append(result.Groups, group{ID: category.ID, Title: category.Title})
 	}
 
-	result.FeedsGroups = c.buildFeedGroups(feeds)
+	result.FeedsGroups = h.buildFeedGroups(feeds)
 	result.SetCommonValues()
 	json.OK(w, r, result)
 }
@@ -228,11 +130,11 @@ should be limited to feeds with an is_spark equal to 0.
 
 For the “Sparks” super group the items should be limited to feeds with an is_spark equal to 1.
 */
-func (c *Controller) handleFeeds(w http.ResponseWriter, r *http.Request) {
+func (h *handler) handleFeeds(w http.ResponseWriter, r *http.Request) {
 	userID := request.UserID(r)
 	logger.Debug("[Fever] Fetching feeds for userID=%d", userID)
 
-	feeds, err := c.store.Feeds(userID)
+	feeds, err := h.store.Feeds(userID)
 	if err != nil {
 		json.ServerError(w, r, err)
 		return
@@ -257,7 +159,7 @@ func (c *Controller) handleFeeds(w http.ResponseWriter, r *http.Request) {
 		result.Feeds = append(result.Feeds, subscripion)
 	}
 
-	result.FeedsGroups = c.buildFeedGroups(feeds)
+	result.FeedsGroups = h.buildFeedGroups(feeds)
 	result.SetCommonValues()
 	json.OK(w, r, result)
 }
@@ -281,11 +183,11 @@ A PHP/HTML example:
 
 	echo '<img src="data:'.$favicon['data'].'">';
 */
-func (c *Controller) handleFavicons(w http.ResponseWriter, r *http.Request) {
+func (h *handler) handleFavicons(w http.ResponseWriter, r *http.Request) {
 	userID := request.UserID(r)
 	logger.Debug("[Fever] Fetching favicons for userID=%d", userID)
 
-	icons, err := c.store.Icons(userID)
+	icons, err := h.store.Icons(userID)
 	if err != nil {
 		json.ServerError(w, r, err)
 		return
@@ -334,13 +236,13 @@ Three optional arguments control determine the items included in the response.
 	(added in API version 2)
 
 */
-func (c *Controller) handleItems(w http.ResponseWriter, r *http.Request) {
+func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
 	var result itemsResponse
 
 	userID := request.UserID(r)
 	logger.Debug("[Fever] Fetching items for userID=%d", userID)
 
-	builder := c.store.NewEntryQueryBuilder(userID)
+	builder := h.store.NewEntryQueryBuilder(userID)
 	builder.WithoutStatus(model.EntryStatusRemoved)
 	builder.WithLimit(50)
 	builder.WithOrder("id")
@@ -375,7 +277,7 @@ func (c *Controller) handleItems(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	builder = c.store.NewEntryQueryBuilder(userID)
+	builder = h.store.NewEntryQueryBuilder(userID)
 	builder.WithoutStatus(model.EntryStatusRemoved)
 	result.Total, err = builder.CountEntries()
 	if err != nil {
@@ -419,11 +321,11 @@ with the remote Fever installation.
 A request with the unread_item_ids argument will return one additional member:
     unread_item_ids (string/comma-separated list of positive integers)
 */
-func (c *Controller) handleUnreadItems(w http.ResponseWriter, r *http.Request) {
+func (h *handler) handleUnreadItems(w http.ResponseWriter, r *http.Request) {
 	userID := request.UserID(r)
 	logger.Debug("[Fever] Fetching unread items for userID=%d", userID)
 
-	builder := c.store.NewEntryQueryBuilder(userID)
+	builder := h.store.NewEntryQueryBuilder(userID)
 	builder.WithStatus(model.EntryStatusUnread)
 	entries, err := builder.GetEntries()
 	if err != nil {
@@ -450,11 +352,11 @@ with the remote Fever installation.
 
 	saved_item_ids (string/comma-separated list of positive integers)
 */
-func (c *Controller) handleSavedItems(w http.ResponseWriter, r *http.Request) {
+func (h *handler) handleSavedItems(w http.ResponseWriter, r *http.Request) {
 	userID := request.UserID(r)
 	logger.Debug("[Fever] Fetching saved items for userID=%d", userID)
 
-	builder := c.store.NewEntryQueryBuilder(userID)
+	builder := h.store.NewEntryQueryBuilder(userID)
 	builder.WithStarred()
 
 	entryIDs, err := builder.GetEntryIDs()
@@ -478,7 +380,7 @@ func (c *Controller) handleSavedItems(w http.ResponseWriter, r *http.Request) {
 	as=? where ? is replaced with read, saved or unsaved
 	id=? where ? is replaced with the id of the item to modify
 */
-func (c *Controller) handleWriteItems(w http.ResponseWriter, r *http.Request) {
+func (h *handler) handleWriteItems(w http.ResponseWriter, r *http.Request) {
 	userID := request.UserID(r)
 	logger.Debug("[Fever] Receiving mark=item call for userID=%d", userID)
 
@@ -487,7 +389,7 @@ func (c *Controller) handleWriteItems(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	builder := c.store.NewEntryQueryBuilder(userID)
+	builder := h.store.NewEntryQueryBuilder(userID)
 	builder.WithEntryID(entryID)
 	builder.WithoutStatus(model.EntryStatusRemoved)
 
@@ -504,25 +406,25 @@ func (c *Controller) handleWriteItems(w http.ResponseWriter, r *http.Request) {
 	switch r.FormValue("as") {
 	case "read":
 		logger.Debug("[Fever] Mark entry #%d as read", entryID)
-		c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusRead)
+		h.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusRead)
 	case "unread":
 		logger.Debug("[Fever] Mark entry #%d as unread", entryID)
-		c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread)
+		h.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread)
 	case "saved", "unsaved":
 		logger.Debug("[Fever] Mark entry #%d as saved/unsaved", entryID)
-		if err := c.store.ToggleBookmark(userID, entryID); err != nil {
+		if err := h.store.ToggleBookmark(userID, entryID); err != nil {
 			json.ServerError(w, r, err)
 			return
 		}
 
-		settings, err := c.store.Integration(userID)
+		settings, err := h.store.Integration(userID)
 		if err != nil {
 			json.ServerError(w, r, err)
 			return
 		}
 
 		go func() {
-			integration.SendEntry(c.cfg, entry, settings)
+			integration.SendEntry(h.cfg, entry, settings)
 		}()
 	}
 
@@ -535,7 +437,7 @@ func (c *Controller) handleWriteItems(w http.ResponseWriter, r *http.Request) {
 	id=? where ? is replaced with the id of the feed or group to modify
 	before=? where ? is replaced with the Unix timestamp of the the local client’s most recent items API request
 */
-func (c *Controller) handleWriteFeeds(w http.ResponseWriter, r *http.Request) {
+func (h *handler) handleWriteFeeds(w http.ResponseWriter, r *http.Request) {
 	userID := request.UserID(r)
 	feedID := request.FormInt64Value(r, "id")
 	before := time.Unix(request.FormInt64Value(r, "before"), 0)
@@ -547,7 +449,7 @@ func (c *Controller) handleWriteFeeds(w http.ResponseWriter, r *http.Request) {
 	}
 
 	go func() {
-		if err := c.store.MarkFeedAsRead(userID, feedID, before); err != nil {
+		if err := h.store.MarkFeedAsRead(userID, feedID, before); err != nil {
 			logger.Error("[Fever] MarkFeedAsRead failed: %v", err)
 		}
 	}()
@@ -561,7 +463,7 @@ func (c *Controller) handleWriteFeeds(w http.ResponseWriter, r *http.Request) {
 	id=? where ? is replaced with the id of the feed or group to modify
 	before=? where ? is replaced with the Unix timestamp of the the local client’s most recent items API request
 */
-func (c *Controller) handleWriteGroups(w http.ResponseWriter, r *http.Request) {
+func (h *handler) handleWriteGroups(w http.ResponseWriter, r *http.Request) {
 	userID := request.UserID(r)
 	groupID := request.FormInt64Value(r, "id")
 	before := time.Unix(request.FormInt64Value(r, "before"), 0)
@@ -576,9 +478,9 @@ func (c *Controller) handleWriteGroups(w http.ResponseWriter, r *http.Request) {
 		var err error
 
 		if groupID == 0 {
-			err = c.store.MarkAllAsRead(userID)
+			err = h.store.MarkAllAsRead(userID)
 		} else {
-			err = c.store.MarkCategoryAsRead(userID, groupID, before)
+			err = h.store.MarkCategoryAsRead(userID, groupID, before)
 		}
 
 		if err != nil {
@@ -596,7 +498,7 @@ A feeds_group object has the following members:
     feed_ids (string/comma-separated list of positive integers)
 
 */
-func (c *Controller) buildFeedGroups(feeds model.Feeds) []feedsGroups {
+func (h *handler) buildFeedGroups(feeds model.Feeds) []feedsGroups {
 	feedsGroupedByCategory := make(map[int64][]string)
 	for _, feed := range feeds {
 		feedsGroupedByCategory[feed.Category.ID] = append(feedsGroupedByCategory[feed.Category.ID], strconv.FormatInt(feed.ID, 10))
@@ -612,8 +514,3 @@ func (c *Controller) buildFeedGroups(feeds model.Feeds) []feedsGroups {
 
 	return result
 }
-
-// NewController returns a new Fever API.
-func NewController(cfg *config.Config, store *storage.Storage) *Controller {
-	return &Controller{cfg, store}
-}
diff --git a/middleware/fever.go b/fever/middleware.go
similarity index 60%
rename from middleware/fever.go
rename to fever/middleware.go
index 7b96677f8f4089885d5719235a44c1cb02f9e004..28dac7db1cd50e8e1f9a2c5d4f9d65252bb3ed87 100644
--- a/middleware/fever.go
+++ b/fever/middleware.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by the Apache 2.0
 // license that can be found in the LICENSE file.
 
-package middleware // import "miniflux.app/middleware"
+package fever // import "miniflux.app/fever"
 
 import (
 	"context"
@@ -11,34 +11,40 @@ import (
 	"miniflux.app/http/request"
 	"miniflux.app/http/response/json"
 	"miniflux.app/logger"
+	"miniflux.app/storage"
 )
 
-var feverAuthFailureResponse = map[string]int{"api_version": 3, "auth": 0}
+type middleware struct {
+	store *storage.Storage
+}
+
+func newMiddleware(s *storage.Storage) *middleware {
+	return &middleware{s}
+}
 
-// FeverAuth handles Fever API authentication.
-func (m *Middleware) FeverAuth(next http.Handler) http.Handler {
+func (m *middleware) serve(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		apiKey := r.FormValue("api_key")
 		if apiKey == "" {
-			logger.Info("[Middleware:Fever] No API key provided")
-			json.OK(w, r, feverAuthFailureResponse)
+			logger.Info("[Fever] No API key provided")
+			json.OK(w, r, newAuthFailureResponse())
 			return
 		}
 
 		user, err := m.store.UserByFeverToken(apiKey)
 		if err != nil {
-			logger.Error("[Middleware:Fever] %v", err)
-			json.OK(w, r, feverAuthFailureResponse)
+			logger.Error("[Fever] %v", err)
+			json.OK(w, r, newAuthFailureResponse())
 			return
 		}
 
 		if user == nil {
-			logger.Info("[Middleware:Fever] No user found with this API key")
-			json.OK(w, r, feverAuthFailureResponse)
+			logger.Info("[Fever] No user found with this API key")
+			json.OK(w, r, newAuthFailureResponse())
 			return
 		}
 
-		logger.Info("[Middleware:Fever] User #%d is authenticated", user.ID)
+		logger.Info("[Fever] User #%d is authenticated", user.ID)
 		m.store.SetLastLogin(user.ID)
 
 		ctx := r.Context()
diff --git a/fever/response.go b/fever/response.go
new file mode 100644
index 0000000000000000000000000000000000000000..444af81d066316344bfce5eb5211ed0a09a89764
--- /dev/null
+++ b/fever/response.go
@@ -0,0 +1,120 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package fever // import "miniflux.app/fever"
+
+import (
+	"time"
+)
+
+type baseResponse struct {
+	Version       int   `json:"api_version"`
+	Authenticated int   `json:"auth"`
+	LastRefresh   int64 `json:"last_refreshed_on_time"`
+}
+
+func (b *baseResponse) SetCommonValues() {
+	b.Version = 3
+	b.Authenticated = 1
+	b.LastRefresh = time.Now().Unix()
+}
+
+/*
+The default response is a JSON object containing two members:
+
+    api_version contains the version of the API responding (positive integer)
+    auth whether the request was successfully authenticated (boolean integer)
+
+The API can also return XML by passing xml as the optional value of the api argument like so:
+
+http://yourdomain.com/fever/?api=xml
+
+The top level XML element is named response.
+
+The response to each successfully authenticated request will have auth set to 1 and include
+at least one additional member:
+
+	last_refreshed_on_time contains the time of the most recently refreshed (not updated)
+	feed (Unix timestamp/integer)
+
+*/
+func newBaseResponse() baseResponse {
+	r := baseResponse{}
+	r.SetCommonValues()
+	return r
+}
+
+func newAuthFailureResponse() baseResponse {
+	return baseResponse{Version: 3, Authenticated: 0}
+}
+
+type groupsResponse struct {
+	baseResponse
+	Groups      []group       `json:"groups"`
+	FeedsGroups []feedsGroups `json:"feeds_groups"`
+}
+
+type feedsResponse struct {
+	baseResponse
+	Feeds       []feed        `json:"feeds"`
+	FeedsGroups []feedsGroups `json:"feeds_groups"`
+}
+
+type faviconsResponse struct {
+	baseResponse
+	Favicons []favicon `json:"favicons"`
+}
+
+type itemsResponse struct {
+	baseResponse
+	Items []item `json:"items"`
+	Total int    `json:"total_items"`
+}
+
+type unreadResponse struct {
+	baseResponse
+	ItemIDs string `json:"unread_item_ids"`
+}
+
+type savedResponse struct {
+	baseResponse
+	ItemIDs string `json:"saved_item_ids"`
+}
+
+type group struct {
+	ID    int64  `json:"id"`
+	Title string `json:"title"`
+}
+
+type feedsGroups struct {
+	GroupID int64  `json:"group_id"`
+	FeedIDs string `json:"feed_ids"`
+}
+
+type feed struct {
+	ID          int64  `json:"id"`
+	FaviconID   int64  `json:"favicon_id"`
+	Title       string `json:"title"`
+	URL         string `json:"url"`
+	SiteURL     string `json:"site_url"`
+	IsSpark     int    `json:"is_spark"`
+	LastUpdated int64  `json:"last_updated_on_time"`
+}
+
+type item struct {
+	ID        int64  `json:"id"`
+	FeedID    int64  `json:"feed_id"`
+	Title     string `json:"title"`
+	Author    string `json:"author"`
+	HTML      string `json:"html"`
+	URL       string `json:"url"`
+	IsSaved   int    `json:"is_saved"`
+	IsRead    int    `json:"is_read"`
+	CreatedAt int64  `json:"created_on_time"`
+}
+
+type favicon struct {
+	ID   int64  `json:"id"`
+	Data string `json:"data"`
+}