From 5a69a61d4841a35d7ddcb761a688db8c688314d6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= <fred@miniflux.net>
Date: Sun, 11 Nov 2018 11:28:29 -0800
Subject: [PATCH] Move UI middlewares and routes to ui package

---
 daemon/routes.go                 |  84 +----------------
 middleware/app_session.go        |  76 ----------------
 middleware/user_session.go       |  77 ----------------
 ui/about.go                      |  13 ++-
 ui/bookmark_entries.go           |  19 ++--
 ui/category_create.go            |  13 ++-
 ui/category_edit.go              |  15 ++--
 ui/category_entries.go           |  21 +++--
 ui/category_list.go              |  15 ++--
 ui/category_remove.go            |  11 ++-
 ui/category_save.go              |  19 ++--
 ui/category_update.go            |  21 +++--
 ui/entry_bookmark.go             |  25 +++---
 ui/entry_category.go             |  25 +++---
 ui/entry_feed.go                 |  25 +++---
 ui/entry_read.go                 |  23 +++--
 ui/entry_save.go                 |   9 +-
 ui/entry_scraper.go              |   7 +-
 ui/entry_search.go               |  25 +++---
 ui/entry_toggle_bookmark.go      |   5 +-
 ui/entry_unread.go               |  27 +++---
 ui/entry_update_status.go        |   5 +-
 ui/feed_edit.go                  |  17 ++--
 ui/feed_entries.go               |  21 +++--
 ui/feed_icon.go                  |   5 +-
 ui/feed_list.go                  |  15 ++--
 ui/feed_refresh.go               |  16 ++--
 ui/feed_remove.go                |   7 +-
 ui/feed_update.go                |  21 +++--
 ui/{controller.go => handler.go} |  19 +---
 ui/history_entries.go            |  19 ++--
 ui/history_flush.go              |   7 +-
 ui/integration_pocket.go         |  32 ++++---
 ui/integration_show.go           |  17 ++--
 ui/integration_update.go         |  17 ++--
 ui/login_check.go                |  21 +++--
 ui/login_show.go                 |   9 +-
 ui/logout.go                     |  15 ++--
 ui/middleware.go                 | 149 +++++++++++++++++++++++++++++++
 ui/oauth2_callback.go            |  41 +++++----
 ui/oauth2_redirect.go            |  11 ++-
 ui/oauth2_unlink.go              |  19 ++--
 ui/opml_export.go                |   5 +-
 ui/opml_import.go                |  13 ++-
 ui/opml_upload.go                |  19 ++--
 ui/pagination.go                 |   4 +-
 ui/proxy.go                      |   3 +-
 ui/search_entries.go             |  19 ++--
 ui/session_list.go               |  15 ++--
 ui/session_remove.go             |   7 +-
 ui/settings_show.go              |  15 ++--
 ui/settings_update.go            |  21 +++--
 ui/static_app_icon.go            |   3 +-
 ui/static_favicon.go             |   3 +-
 ui/static_javascript.go          |   3 +-
 ui/static_manifest.go            |  11 ++-
 ui/static_stylesheet.go          |   3 +-
 ui/subscription_add.go           |  15 ++--
 ui/subscription_bookmarklet.go   |  15 ++--
 ui/subscription_choose.go        |  19 ++--
 ui/subscription_submit.go        |  25 +++---
 ui/ui.go                         | 133 +++++++++++++++++++++++++++
 ui/unread_entries.go             |  19 ++--
 ui/unread_mark_all_read.go       |   7 +-
 ui/user_create.go                |  13 ++-
 ui/user_edit.go                  |  14 +--
 ui/user_list.go                  |  15 ++--
 ui/user_remove.go                |  11 ++-
 ui/user_save.go                  |  19 ++--
 ui/user_update.go                |  21 +++--
 70 files changed, 739 insertions(+), 769 deletions(-)
 delete mode 100644 middleware/app_session.go
 delete mode 100644 middleware/user_session.go
 rename ui/{controller.go => handler.go} (54%)
 create mode 100644 ui/middleware.go
 create mode 100644 ui/ui.go

diff --git a/daemon/routes.go b/daemon/routes.go
index b4b923c7..b7cfedcc 100644
--- a/daemon/routes.go
+++ b/daemon/routes.go
@@ -5,8 +5,6 @@
 package daemon // import "miniflux.app/daemon"
 
 import (
-	"net/http"
-
 	"miniflux.app/api"
 	"miniflux.app/config"
 	"miniflux.app/fever"
@@ -14,7 +12,6 @@ import (
 	"miniflux.app/reader/feed"
 	"miniflux.app/scheduler"
 	"miniflux.app/storage"
-	"miniflux.app/template"
 	"miniflux.app/ui"
 
 	"github.com/gorilla/mux"
@@ -22,8 +19,6 @@ import (
 
 func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handler, pool *scheduler.WorkerPool) *mux.Router {
 	router := mux.NewRouter()
-	templateEngine := template.NewEngine(cfg, router)
-	uiController := ui.NewController(cfg, store, pool, feedHandler, templateEngine, router)
 	middleware := middleware.New(cfg, store, router)
 
 	if cfg.BasePath() != "" {
@@ -34,86 +29,9 @@ func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handle
 	router.Use(middleware.HeaderConfig)
 	router.Use(middleware.Logging)
 
-	router.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
-		w.Write([]byte("OK"))
-	})
-
-	router.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
-		w.Header().Set("Content-Type", "text/plain")
-		w.Write([]byte("User-agent: *\nDisallow: /"))
-	})
-
 	fever.Serve(router, cfg, store)
 	api.Serve(router, store, feedHandler)
-
-	uiRouter := router.NewRoute().Subrouter()
-	uiRouter.Use(middleware.AppSession)
-	uiRouter.Use(middleware.UserSession)
-	uiRouter.HandleFunc("/stylesheets/{name}.css", uiController.Stylesheet).Name("stylesheet").Methods("GET")
-	uiRouter.HandleFunc("/{name}.js", uiController.Javascript).Name("javascript").Methods("GET")
-	uiRouter.HandleFunc("/favicon.ico", uiController.Favicon).Name("favicon").Methods("GET")
-	uiRouter.HandleFunc("/icon/{filename}", uiController.AppIcon).Name("appIcon").Methods("GET")
-	uiRouter.HandleFunc("/manifest.json", uiController.WebManifest).Name("webManifest").Methods("GET")
-	uiRouter.HandleFunc("/subscribe", uiController.AddSubscription).Name("addSubscription").Methods("GET")
-	uiRouter.HandleFunc("/subscribe", uiController.SubmitSubscription).Name("submitSubscription").Methods("POST")
-	uiRouter.HandleFunc("/subscriptions", uiController.ChooseSubscription).Name("chooseSubscription").Methods("POST")
-	uiRouter.HandleFunc("/mark-all-as-read", uiController.MarkAllAsRead).Name("markAllAsRead").Methods("GET")
-	uiRouter.HandleFunc("/unread", uiController.ShowUnreadPage).Name("unread").Methods("GET")
-	uiRouter.HandleFunc("/history", uiController.ShowHistoryPage).Name("history").Methods("GET")
-	uiRouter.HandleFunc("/starred", uiController.ShowStarredPage).Name("starred").Methods("GET")
-	uiRouter.HandleFunc("/search", uiController.ShowSearchEntries).Name("searchEntries").Methods("GET")
-	uiRouter.HandleFunc("/search/entry/{entryID}", uiController.ShowSearchEntry).Name("searchEntry").Methods("GET")
-	uiRouter.HandleFunc("/feed/{feedID}/refresh", uiController.RefreshFeed).Name("refreshFeed").Methods("GET")
-	uiRouter.HandleFunc("/feed/{feedID}/edit", uiController.EditFeed).Name("editFeed").Methods("GET")
-	uiRouter.HandleFunc("/feed/{feedID}/remove", uiController.RemoveFeed).Name("removeFeed").Methods("POST")
-	uiRouter.HandleFunc("/feed/{feedID}/update", uiController.UpdateFeed).Name("updateFeed").Methods("POST")
-	uiRouter.HandleFunc("/feed/{feedID}/entries", uiController.ShowFeedEntries).Name("feedEntries").Methods("GET")
-	uiRouter.HandleFunc("/feeds", uiController.ShowFeedsPage).Name("feeds").Methods("GET")
-	uiRouter.HandleFunc("/feeds/refresh", uiController.RefreshAllFeeds).Name("refreshAllFeeds").Methods("GET")
-	uiRouter.HandleFunc("/unread/entry/{entryID}", uiController.ShowUnreadEntry).Name("unreadEntry").Methods("GET")
-	uiRouter.HandleFunc("/history/entry/{entryID}", uiController.ShowReadEntry).Name("readEntry").Methods("GET")
-	uiRouter.HandleFunc("/history/flush", uiController.FlushHistory).Name("flushHistory").Methods("GET")
-	uiRouter.HandleFunc("/feed/{feedID}/entry/{entryID}", uiController.ShowFeedEntry).Name("feedEntry").Methods("GET")
-	uiRouter.HandleFunc("/category/{categoryID}/entry/{entryID}", uiController.ShowCategoryEntry).Name("categoryEntry").Methods("GET")
-	uiRouter.HandleFunc("/starred/entry/{entryID}", uiController.ShowStarredEntry).Name("starredEntry").Methods("GET")
-	uiRouter.HandleFunc("/entry/status", uiController.UpdateEntriesStatus).Name("updateEntriesStatus").Methods("POST")
-	uiRouter.HandleFunc("/entry/save/{entryID}", uiController.SaveEntry).Name("saveEntry").Methods("POST")
-	uiRouter.HandleFunc("/entry/download/{entryID}", uiController.FetchContent).Name("fetchContent").Methods("POST")
-	uiRouter.HandleFunc("/entry/bookmark/{entryID}", uiController.ToggleBookmark).Name("toggleBookmark").Methods("POST")
-	uiRouter.HandleFunc("/categories", uiController.CategoryList).Name("categories").Methods("GET")
-	uiRouter.HandleFunc("/category/create", uiController.CreateCategory).Name("createCategory").Methods("GET")
-	uiRouter.HandleFunc("/category/save", uiController.SaveCategory).Name("saveCategory").Methods("POST")
-	uiRouter.HandleFunc("/category/{categoryID}/entries", uiController.CategoryEntries).Name("categoryEntries").Methods("GET")
-	uiRouter.HandleFunc("/category/{categoryID}/edit", uiController.EditCategory).Name("editCategory").Methods("GET")
-	uiRouter.HandleFunc("/category/{categoryID}/update", uiController.UpdateCategory).Name("updateCategory").Methods("POST")
-	uiRouter.HandleFunc("/category/{categoryID}/remove", uiController.RemoveCategory).Name("removeCategory").Methods("POST")
-	uiRouter.HandleFunc("/feed/icon/{iconID}", uiController.ShowIcon).Name("icon").Methods("GET")
-	uiRouter.HandleFunc("/proxy/{encodedURL}", uiController.ImageProxy).Name("proxy").Methods("GET")
-	uiRouter.HandleFunc("/users", uiController.ShowUsers).Name("users").Methods("GET")
-	uiRouter.HandleFunc("/user/create", uiController.CreateUser).Name("createUser").Methods("GET")
-	uiRouter.HandleFunc("/user/save", uiController.SaveUser).Name("saveUser").Methods("POST")
-	uiRouter.HandleFunc("/users/{userID}/edit", uiController.EditUser).Name("editUser").Methods("GET")
-	uiRouter.HandleFunc("/users/{userID}/update", uiController.UpdateUser).Name("updateUser").Methods("POST")
-	uiRouter.HandleFunc("/users/{userID}/remove", uiController.RemoveUser).Name("removeUser").Methods("POST")
-	uiRouter.HandleFunc("/about", uiController.About).Name("about").Methods("GET")
-	uiRouter.HandleFunc("/settings", uiController.ShowSettings).Name("settings").Methods("GET")
-	uiRouter.HandleFunc("/settings", uiController.UpdateSettings).Name("updateSettings").Methods("POST")
-	uiRouter.HandleFunc("/bookmarklet", uiController.Bookmarklet).Name("bookmarklet").Methods("GET")
-	uiRouter.HandleFunc("/integrations", uiController.ShowIntegrations).Name("integrations").Methods("GET")
-	uiRouter.HandleFunc("/integration", uiController.UpdateIntegration).Name("updateIntegration").Methods("POST")
-	uiRouter.HandleFunc("/integration/pocket/authorize", uiController.PocketAuthorize).Name("pocketAuthorize").Methods("GET")
-	uiRouter.HandleFunc("/integration/pocket/callback", uiController.PocketCallback).Name("pocketCallback").Methods("GET")
-	uiRouter.HandleFunc("/sessions", uiController.ShowSessions).Name("sessions").Methods("GET")
-	uiRouter.HandleFunc("/sessions/{sessionID}/remove", uiController.RemoveSession).Name("removeSession").Methods("POST")
-	uiRouter.HandleFunc("/export", uiController.Export).Name("export").Methods("GET")
-	uiRouter.HandleFunc("/import", uiController.Import).Name("import").Methods("GET")
-	uiRouter.HandleFunc("/upload", uiController.UploadOPML).Name("uploadOPML").Methods("POST")
-	uiRouter.HandleFunc("/oauth2/{provider}/unlink", uiController.OAuth2Unlink).Name("oauth2Unlink").Methods("GET")
-	uiRouter.HandleFunc("/oauth2/{provider}/redirect", uiController.OAuth2Redirect).Name("oauth2Redirect").Methods("GET")
-	uiRouter.HandleFunc("/oauth2/{provider}/callback", uiController.OAuth2Callback).Name("oauth2Callback").Methods("GET")
-	uiRouter.HandleFunc("/login", uiController.CheckLogin).Name("checkLogin").Methods("POST")
-	uiRouter.HandleFunc("/logout", uiController.Logout).Name("logout").Methods("GET")
-	uiRouter.HandleFunc("/", uiController.ShowLoginPage).Name("login").Methods("GET")
+	ui.Serve(router, cfg, store, pool, feedHandler)
 
 	return router
 }
diff --git a/middleware/app_session.go b/middleware/app_session.go
deleted file mode 100644
index 1134e399..00000000
--- a/middleware/app_session.go
+++ /dev/null
@@ -1,76 +0,0 @@
-// 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 middleware // import "miniflux.app/middleware"
-
-import (
-	"context"
-	"errors"
-	"net/http"
-
-	"miniflux.app/http/cookie"
-	"miniflux.app/http/request"
-	"miniflux.app/http/response/html"
-	"miniflux.app/logger"
-	"miniflux.app/model"
-)
-
-// AppSession handles application session middleware.
-func (m *Middleware) AppSession(next http.Handler) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		var err error
-		session := m.getAppSessionValueFromCookie(r)
-
-		if session == nil {
-			logger.Debug("[Middleware:AppSession] Session not found")
-
-			session, err = m.store.CreateSession()
-			if err != nil {
-				html.ServerError(w, r, err)
-				return
-			}
-
-			http.SetCookie(w, cookie.New(cookie.CookieSessionID, session.ID, m.cfg.IsHTTPS, m.cfg.BasePath()))
-		} else {
-			logger.Debug("[Middleware:AppSession] %s", session)
-		}
-
-		if r.Method == "POST" {
-			formValue := r.FormValue("csrf")
-			headerValue := r.Header.Get("X-Csrf-Token")
-
-			if session.Data.CSRF != formValue && session.Data.CSRF != headerValue {
-				logger.Error(`[Middleware:AppSession] Invalid or missing CSRF token: Form="%s", Header="%s"`, formValue, headerValue)
-				html.BadRequest(w, r, errors.New("Invalid or missing CSRF"))
-				return
-			}
-		}
-
-		ctx := r.Context()
-		ctx = context.WithValue(ctx, request.SessionIDContextKey, session.ID)
-		ctx = context.WithValue(ctx, request.CSRFContextKey, session.Data.CSRF)
-		ctx = context.WithValue(ctx, request.OAuth2StateContextKey, session.Data.OAuth2State)
-		ctx = context.WithValue(ctx, request.FlashMessageContextKey, session.Data.FlashMessage)
-		ctx = context.WithValue(ctx, request.FlashErrorMessageContextKey, session.Data.FlashErrorMessage)
-		ctx = context.WithValue(ctx, request.UserLanguageContextKey, session.Data.Language)
-		ctx = context.WithValue(ctx, request.UserThemeContextKey, session.Data.Theme)
-		ctx = context.WithValue(ctx, request.PocketRequestTokenContextKey, session.Data.PocketRequestToken)
-		next.ServeHTTP(w, r.WithContext(ctx))
-	})
-}
-
-func (m *Middleware) getAppSessionValueFromCookie(r *http.Request) *model.Session {
-	cookieValue := request.CookieValue(r, cookie.CookieSessionID)
-	if cookieValue == "" {
-		return nil
-	}
-
-	session, err := m.store.Session(cookieValue)
-	if err != nil {
-		logger.Error("[Middleware:AppSession] %v", err)
-		return nil
-	}
-
-	return session
-}
diff --git a/middleware/user_session.go b/middleware/user_session.go
deleted file mode 100644
index 10eeca7d..00000000
--- a/middleware/user_session.go
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright 2017 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 middleware // import "miniflux.app/middleware"
-
-import (
-	"context"
-	"net/http"
-
-	"miniflux.app/http/cookie"
-	"miniflux.app/http/request"
-	"miniflux.app/http/response/html"
-	"miniflux.app/http/route"
-	"miniflux.app/logger"
-	"miniflux.app/model"
-
-	"github.com/gorilla/mux"
-)
-
-// UserSession handles the user session middleware.
-func (m *Middleware) UserSession(next http.Handler) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		session := m.getUserSessionFromCookie(r)
-
-		if session == nil {
-			logger.Debug("[Middleware:UserSession] Session not found")
-			if m.isPublicRoute(r) {
-				next.ServeHTTP(w, r)
-			} else {
-				html.Redirect(w, r, route.Path(m.router, "login"))
-			}
-		} else {
-			logger.Debug("[Middleware:UserSession] %s", session)
-
-			ctx := r.Context()
-			ctx = context.WithValue(ctx, request.UserIDContextKey, session.UserID)
-			ctx = context.WithValue(ctx, request.IsAuthenticatedContextKey, true)
-			ctx = context.WithValue(ctx, request.UserSessionTokenContextKey, session.Token)
-
-			next.ServeHTTP(w, r.WithContext(ctx))
-		}
-	})
-}
-
-func (m *Middleware) isPublicRoute(r *http.Request) bool {
-	route := mux.CurrentRoute(r)
-	switch route.GetName() {
-	case "login",
-		"checkLogin",
-		"stylesheet",
-		"javascript",
-		"oauth2Redirect",
-		"oauth2Callback",
-		"appIcon",
-		"favicon",
-		"webManifest":
-		return true
-	default:
-		return false
-	}
-}
-
-func (m *Middleware) getUserSessionFromCookie(r *http.Request) *model.UserSession {
-	cookieValue := request.CookieValue(r, cookie.CookieUserSessionID)
-	if cookieValue == "" {
-		return nil
-	}
-
-	session, err := m.store.UserSessionByToken(cookieValue)
-	if err != nil {
-		logger.Error("[Middleware:UserSession] %v", err)
-		return nil
-	}
-
-	return session
-}
diff --git a/ui/about.go b/ui/about.go
index abc39a0f..5bb9f6b3 100644
--- a/ui/about.go
+++ b/ui/about.go
@@ -14,22 +14,21 @@ import (
 	"miniflux.app/version"
 )
 
-// About shows the about page.
-func (c *Controller) About(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showAboutPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("version", version.Version)
 	view.Set("build_date", version.BuildDate)
 	view.Set("menu", "settings")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 
 	html.OK(w, r, view.Render("about"))
 }
diff --git a/ui/bookmark_entries.go b/ui/bookmark_entries.go
index 5e6bb45e..f2c36b29 100644
--- a/ui/bookmark_entries.go
+++ b/ui/bookmark_entries.go
@@ -15,16 +15,15 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ShowStarredPage renders the page with all starred entries.
-func (c *Controller) ShowStarredPage(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showStarredPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	offset := request.QueryIntParam(r, "offset", 0)
-	builder := c.store.NewEntryQueryBuilder(user.ID)
+	builder := h.store.NewEntryQueryBuilder(user.ID)
 	builder.WithoutStatus(model.EntryStatusRemoved)
 	builder.WithStarred()
 	builder.WithOrder(model.DefaultSortingOrder)
@@ -44,17 +43,17 @@ func (c *Controller) ShowStarredPage(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 
 	view.Set("total", count)
 	view.Set("entries", entries)
-	view.Set("pagination", c.getPagination(route.Path(c.router, "starred"), count, offset))
+	view.Set("pagination", getPagination(route.Path(h.router, "starred"), count, offset))
 	view.Set("menu", "starred")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
-	view.Set("hasSaveEntry", c.store.HasSaveEntry(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
+	view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
 
 	html.OK(w, r, view.Render("bookmark_entries"))
 }
diff --git a/ui/category_create.go b/ui/category_create.go
index bd5039ff..d8fe2cfa 100644
--- a/ui/category_create.go
+++ b/ui/category_create.go
@@ -13,20 +13,19 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// CreateCategory shows the form to create a new category.
-func (c *Controller) CreateCategory(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showCreateCategoryPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("menu", "categories")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 
 	html.OK(w, r, view.Render("create_category"))
 }
diff --git a/ui/category_edit.go b/ui/category_edit.go
index e0375beb..344cfe4e 100644
--- a/ui/category_edit.go
+++ b/ui/category_edit.go
@@ -14,19 +14,18 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// EditCategory shows the form to modify a category.
-func (c *Controller) EditCategory(w http.ResponseWriter, r *http.Request) {
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+func (h *handler) showEditCategoryPage(w http.ResponseWriter, r *http.Request) {
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 
-	user, err := c.store.UserByID(request.UserID(r))
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	categoryID := request.RouteInt64Param(r, "categoryID")
-	category, err := c.store.Category(request.UserID(r), categoryID)
+	category, err := h.store.Category(request.UserID(r), categoryID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -45,8 +44,8 @@ func (c *Controller) EditCategory(w http.ResponseWriter, r *http.Request) {
 	view.Set("category", category)
 	view.Set("menu", "categories")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 
 	html.OK(w, r, view.Render("edit_category"))
 }
diff --git a/ui/category_entries.go b/ui/category_entries.go
index fe5f6385..19b17b9b 100644
--- a/ui/category_entries.go
+++ b/ui/category_entries.go
@@ -15,16 +15,15 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// CategoryEntries shows all entries for the given category.
-func (c *Controller) CategoryEntries(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showCategoryEntriesPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	categoryID := request.RouteInt64Param(r, "categoryID")
-	category, err := c.store.Category(request.UserID(r), categoryID)
+	category, err := h.store.Category(request.UserID(r), categoryID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -36,7 +35,7 @@ func (c *Controller) CategoryEntries(w http.ResponseWriter, r *http.Request) {
 	}
 
 	offset := request.QueryIntParam(r, "offset", 0)
-	builder := c.store.NewEntryQueryBuilder(user.ID)
+	builder := h.store.NewEntryQueryBuilder(user.ID)
 	builder.WithCategoryID(category.ID)
 	builder.WithOrder(model.DefaultSortingOrder)
 	builder.WithDirection(user.EntryDirection)
@@ -56,17 +55,17 @@ func (c *Controller) CategoryEntries(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("category", category)
 	view.Set("total", count)
 	view.Set("entries", entries)
-	view.Set("pagination", c.getPagination(route.Path(c.router, "categoryEntries", "categoryID", category.ID), count, offset))
+	view.Set("pagination", getPagination(route.Path(h.router, "categoryEntries", "categoryID", category.ID), count, offset))
 	view.Set("menu", "categories")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
-	view.Set("hasSaveEntry", c.store.HasSaveEntry(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
+	view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
 
 	html.OK(w, r, view.Render("category_entries"))
 }
diff --git a/ui/category_list.go b/ui/category_list.go
index f4a3651e..3137b956 100644
--- a/ui/category_list.go
+++ b/ui/category_list.go
@@ -13,28 +13,27 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// CategoryList shows the page with all categories.
-func (c *Controller) CategoryList(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showCategoryListPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	categories, err := c.store.CategoriesWithFeedCount(user.ID)
+	categories, err := h.store.CategoriesWithFeedCount(user.ID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("categories", categories)
 	view.Set("total", len(categories))
 	view.Set("menu", "categories")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 
 	html.OK(w, r, view.Render("categories"))
 }
diff --git a/ui/category_remove.go b/ui/category_remove.go
index f8a38dc6..8e616fc3 100644
--- a/ui/category_remove.go
+++ b/ui/category_remove.go
@@ -12,16 +12,15 @@ import (
 	"miniflux.app/http/route"
 )
 
-// RemoveCategory deletes a category from the database.
-func (c *Controller) RemoveCategory(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) removeCategory(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	categoryID := request.RouteInt64Param(r, "categoryID")
-	category, err := c.store.Category(request.UserID(r), categoryID)
+	category, err := h.store.Category(request.UserID(r), categoryID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -32,10 +31,10 @@ func (c *Controller) RemoveCategory(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if err := c.store.RemoveCategory(user.ID, category.ID); err != nil {
+	if err := h.store.RemoveCategory(user.ID, category.ID); err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	html.Redirect(w, r, route.Path(c.router, "categories"))
+	html.Redirect(w, r, route.Path(h.router, "categories"))
 }
diff --git a/ui/category_save.go b/ui/category_save.go
index d115d0ab..dd715283 100644
--- a/ui/category_save.go
+++ b/ui/category_save.go
@@ -17,9 +17,8 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// SaveCategory validate and save the new category into the database.
-func (c *Controller) SaveCategory(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) saveCategory(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -27,13 +26,13 @@ func (c *Controller) SaveCategory(w http.ResponseWriter, r *http.Request) {
 
 	categoryForm := form.NewCategoryForm(r)
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("form", categoryForm)
 	view.Set("menu", "categories")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 
 	if err := categoryForm.Validate(); err != nil {
 		view.Set("errorMessage", err.Error())
@@ -41,7 +40,7 @@ func (c *Controller) SaveCategory(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	duplicateCategory, err := c.store.CategoryByTitle(user.ID, categoryForm.Title)
+	duplicateCategory, err := h.store.CategoryByTitle(user.ID, categoryForm.Title)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -58,12 +57,12 @@ func (c *Controller) SaveCategory(w http.ResponseWriter, r *http.Request) {
 		UserID: user.ID,
 	}
 
-	if err = c.store.CreateCategory(&category); err != nil {
+	if err = h.store.CreateCategory(&category); err != nil {
 		logger.Error("[Controller:CreateCategory] %v", err)
 		view.Set("errorMessage", "error.unable_to_create_category")
 		html.OK(w, r, view.Render("create_category"))
 		return
 	}
 
-	html.Redirect(w, r, route.Path(c.router, "categories"))
+	html.Redirect(w, r, route.Path(h.router, "categories"))
 }
diff --git a/ui/category_update.go b/ui/category_update.go
index 6ef56484..1d206489 100644
--- a/ui/category_update.go
+++ b/ui/category_update.go
@@ -16,16 +16,15 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// UpdateCategory validates and updates a category.
-func (c *Controller) UpdateCategory(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) updateCategory(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	categoryID := request.RouteInt64Param(r, "categoryID")
-	category, err := c.store.Category(request.UserID(r), categoryID)
+	category, err := h.store.Category(request.UserID(r), categoryID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -38,14 +37,14 @@ func (c *Controller) UpdateCategory(w http.ResponseWriter, r *http.Request) {
 
 	categoryForm := form.NewCategoryForm(r)
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("form", categoryForm)
 	view.Set("category", category)
 	view.Set("menu", "categories")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 
 	if err := categoryForm.Validate(); err != nil {
 		view.Set("errorMessage", err.Error())
@@ -53,13 +52,13 @@ func (c *Controller) UpdateCategory(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if c.store.AnotherCategoryExists(user.ID, category.ID, categoryForm.Title) {
+	if h.store.AnotherCategoryExists(user.ID, category.ID, categoryForm.Title) {
 		view.Set("errorMessage", "error.category_already_exists")
 		html.OK(w, r, view.Render("edit_category"))
 		return
 	}
 
-	err = c.store.UpdateCategory(categoryForm.Merge(category))
+	err = h.store.UpdateCategory(categoryForm.Merge(category))
 	if err != nil {
 		logger.Error("[Controller:UpdateCategory] %v", err)
 		view.Set("errorMessage", "error.unable_to_update_category")
@@ -67,5 +66,5 @@ func (c *Controller) UpdateCategory(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	html.Redirect(w, r, route.Path(c.router, "categories"))
+	html.Redirect(w, r, route.Path(h.router, "categories"))
 }
diff --git a/ui/entry_bookmark.go b/ui/entry_bookmark.go
index 895c2bc3..a07859d7 100644
--- a/ui/entry_bookmark.go
+++ b/ui/entry_bookmark.go
@@ -16,16 +16,15 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ShowStarredEntry shows a single feed entry in "starred" mode.
-func (c *Controller) ShowStarredEntry(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showStarredEntryPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	entryID := request.RouteInt64Param(r, "entryID")
-	builder := c.store.NewEntryQueryBuilder(user.ID)
+	builder := h.store.NewEntryQueryBuilder(user.ID)
 	builder.WithEntryID(entryID)
 	builder.WithoutStatus(model.EntryStatusRemoved)
 
@@ -41,7 +40,7 @@ func (c *Controller) ShowStarredEntry(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if entry.Status == model.EntryStatusUnread {
-		err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+		err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
 		if err != nil {
 			html.ServerError(w, r, err)
 			return
@@ -50,7 +49,7 @@ func (c *Controller) ShowStarredEntry(w http.ResponseWriter, r *http.Request) {
 		entry.Status = model.EntryStatusRead
 	}
 
-	entryPaginationBuilder := storage.NewEntryPaginationBuilder(c.store, user.ID, entry.ID, user.EntryDirection)
+	entryPaginationBuilder := storage.NewEntryPaginationBuilder(h.store, user.ID, entry.ID, user.EntryDirection)
 	entryPaginationBuilder.WithStarred()
 	prevEntry, nextEntry, err := entryPaginationBuilder.Entries()
 	if err != nil {
@@ -60,16 +59,16 @@ func (c *Controller) ShowStarredEntry(w http.ResponseWriter, r *http.Request) {
 
 	nextEntryRoute := ""
 	if nextEntry != nil {
-		nextEntryRoute = route.Path(c.router, "starredEntry", "entryID", nextEntry.ID)
+		nextEntryRoute = route.Path(h.router, "starredEntry", "entryID", nextEntry.ID)
 	}
 
 	prevEntryRoute := ""
 	if prevEntry != nil {
-		prevEntryRoute = route.Path(c.router, "starredEntry", "entryID", prevEntry.ID)
+		prevEntryRoute = route.Path(h.router, "starredEntry", "entryID", prevEntry.ID)
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("entry", entry)
 	view.Set("prevEntry", prevEntry)
 	view.Set("nextEntry", nextEntry)
@@ -77,9 +76,9 @@ func (c *Controller) ShowStarredEntry(w http.ResponseWriter, r *http.Request) {
 	view.Set("prevEntryRoute", prevEntryRoute)
 	view.Set("menu", "starred")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
-	view.Set("hasSaveEntry", c.store.HasSaveEntry(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
+	view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
 
 	html.OK(w, r, view.Render("entry"))
 }
diff --git a/ui/entry_category.go b/ui/entry_category.go
index d11248be..c2602572 100644
--- a/ui/entry_category.go
+++ b/ui/entry_category.go
@@ -16,9 +16,8 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ShowCategoryEntry shows a single feed entry in "category" mode.
-func (c *Controller) ShowCategoryEntry(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showCategoryEntryPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -27,7 +26,7 @@ func (c *Controller) ShowCategoryEntry(w http.ResponseWriter, r *http.Request) {
 	categoryID := request.RouteInt64Param(r, "categoryID")
 	entryID := request.RouteInt64Param(r, "entryID")
 
-	builder := c.store.NewEntryQueryBuilder(user.ID)
+	builder := h.store.NewEntryQueryBuilder(user.ID)
 	builder.WithCategoryID(categoryID)
 	builder.WithEntryID(entryID)
 	builder.WithoutStatus(model.EntryStatusRemoved)
@@ -44,7 +43,7 @@ func (c *Controller) ShowCategoryEntry(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if entry.Status == model.EntryStatusUnread {
-		err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+		err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
 		if err != nil {
 			html.ServerError(w, r, err)
 			return
@@ -53,7 +52,7 @@ func (c *Controller) ShowCategoryEntry(w http.ResponseWriter, r *http.Request) {
 		entry.Status = model.EntryStatusRead
 	}
 
-	entryPaginationBuilder := storage.NewEntryPaginationBuilder(c.store, user.ID, entry.ID, user.EntryDirection)
+	entryPaginationBuilder := storage.NewEntryPaginationBuilder(h.store, user.ID, entry.ID, user.EntryDirection)
 	entryPaginationBuilder.WithCategoryID(categoryID)
 	prevEntry, nextEntry, err := entryPaginationBuilder.Entries()
 	if err != nil {
@@ -63,16 +62,16 @@ func (c *Controller) ShowCategoryEntry(w http.ResponseWriter, r *http.Request) {
 
 	nextEntryRoute := ""
 	if nextEntry != nil {
-		nextEntryRoute = route.Path(c.router, "categoryEntry", "categoryID", categoryID, "entryID", nextEntry.ID)
+		nextEntryRoute = route.Path(h.router, "categoryEntry", "categoryID", categoryID, "entryID", nextEntry.ID)
 	}
 
 	prevEntryRoute := ""
 	if prevEntry != nil {
-		prevEntryRoute = route.Path(c.router, "categoryEntry", "categoryID", categoryID, "entryID", prevEntry.ID)
+		prevEntryRoute = route.Path(h.router, "categoryEntry", "categoryID", categoryID, "entryID", prevEntry.ID)
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("entry", entry)
 	view.Set("prevEntry", prevEntry)
 	view.Set("nextEntry", nextEntry)
@@ -80,9 +79,9 @@ func (c *Controller) ShowCategoryEntry(w http.ResponseWriter, r *http.Request) {
 	view.Set("prevEntryRoute", prevEntryRoute)
 	view.Set("menu", "categories")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
-	view.Set("hasSaveEntry", c.store.HasSaveEntry(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
+	view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
 
 	html.OK(w, r, view.Render("entry"))
 }
diff --git a/ui/entry_feed.go b/ui/entry_feed.go
index 9c28c676..e4ea585d 100644
--- a/ui/entry_feed.go
+++ b/ui/entry_feed.go
@@ -16,9 +16,8 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ShowFeedEntry shows a single feed entry in "feed" mode.
-func (c *Controller) ShowFeedEntry(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showFeedEntryPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -27,7 +26,7 @@ func (c *Controller) ShowFeedEntry(w http.ResponseWriter, r *http.Request) {
 	entryID := request.RouteInt64Param(r, "entryID")
 	feedID := request.RouteInt64Param(r, "feedID")
 
-	builder := c.store.NewEntryQueryBuilder(user.ID)
+	builder := h.store.NewEntryQueryBuilder(user.ID)
 	builder.WithFeedID(feedID)
 	builder.WithEntryID(entryID)
 	builder.WithoutStatus(model.EntryStatusRemoved)
@@ -44,7 +43,7 @@ func (c *Controller) ShowFeedEntry(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if entry.Status == model.EntryStatusUnread {
-		err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+		err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
 		if err != nil {
 			html.ServerError(w, r, err)
 			return
@@ -53,7 +52,7 @@ func (c *Controller) ShowFeedEntry(w http.ResponseWriter, r *http.Request) {
 		entry.Status = model.EntryStatusRead
 	}
 
-	entryPaginationBuilder := storage.NewEntryPaginationBuilder(c.store, user.ID, entry.ID, user.EntryDirection)
+	entryPaginationBuilder := storage.NewEntryPaginationBuilder(h.store, user.ID, entry.ID, user.EntryDirection)
 	entryPaginationBuilder.WithFeedID(feedID)
 	prevEntry, nextEntry, err := entryPaginationBuilder.Entries()
 	if err != nil {
@@ -63,16 +62,16 @@ func (c *Controller) ShowFeedEntry(w http.ResponseWriter, r *http.Request) {
 
 	nextEntryRoute := ""
 	if nextEntry != nil {
-		nextEntryRoute = route.Path(c.router, "feedEntry", "feedID", feedID, "entryID", nextEntry.ID)
+		nextEntryRoute = route.Path(h.router, "feedEntry", "feedID", feedID, "entryID", nextEntry.ID)
 	}
 
 	prevEntryRoute := ""
 	if prevEntry != nil {
-		prevEntryRoute = route.Path(c.router, "feedEntry", "feedID", feedID, "entryID", prevEntry.ID)
+		prevEntryRoute = route.Path(h.router, "feedEntry", "feedID", feedID, "entryID", prevEntry.ID)
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("entry", entry)
 	view.Set("prevEntry", prevEntry)
 	view.Set("nextEntry", nextEntry)
@@ -80,9 +79,9 @@ func (c *Controller) ShowFeedEntry(w http.ResponseWriter, r *http.Request) {
 	view.Set("prevEntryRoute", prevEntryRoute)
 	view.Set("menu", "feeds")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
-	view.Set("hasSaveEntry", c.store.HasSaveEntry(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
+	view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
 
 	html.OK(w, r, view.Render("entry"))
 }
diff --git a/ui/entry_read.go b/ui/entry_read.go
index 208ae3bf..df79d444 100644
--- a/ui/entry_read.go
+++ b/ui/entry_read.go
@@ -16,16 +16,15 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ShowReadEntry shows a single feed entry in "history" mode.
-func (c *Controller) ShowReadEntry(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showReadEntryPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	entryID := request.RouteInt64Param(r, "entryID")
-	builder := c.store.NewEntryQueryBuilder(user.ID)
+	builder := h.store.NewEntryQueryBuilder(user.ID)
 	builder.WithEntryID(entryID)
 	builder.WithoutStatus(model.EntryStatusRemoved)
 
@@ -40,7 +39,7 @@ func (c *Controller) ShowReadEntry(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	entryPaginationBuilder := storage.NewEntryPaginationBuilder(c.store, user.ID, entry.ID, user.EntryDirection)
+	entryPaginationBuilder := storage.NewEntryPaginationBuilder(h.store, user.ID, entry.ID, user.EntryDirection)
 	entryPaginationBuilder.WithStatus(model.EntryStatusRead)
 	prevEntry, nextEntry, err := entryPaginationBuilder.Entries()
 	if err != nil {
@@ -50,16 +49,16 @@ func (c *Controller) ShowReadEntry(w http.ResponseWriter, r *http.Request) {
 
 	nextEntryRoute := ""
 	if nextEntry != nil {
-		nextEntryRoute = route.Path(c.router, "readEntry", "entryID", nextEntry.ID)
+		nextEntryRoute = route.Path(h.router, "readEntry", "entryID", nextEntry.ID)
 	}
 
 	prevEntryRoute := ""
 	if prevEntry != nil {
-		prevEntryRoute = route.Path(c.router, "readEntry", "entryID", prevEntry.ID)
+		prevEntryRoute = route.Path(h.router, "readEntry", "entryID", prevEntry.ID)
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("entry", entry)
 	view.Set("prevEntry", prevEntry)
 	view.Set("nextEntry", nextEntry)
@@ -67,9 +66,9 @@ func (c *Controller) ShowReadEntry(w http.ResponseWriter, r *http.Request) {
 	view.Set("prevEntryRoute", prevEntryRoute)
 	view.Set("menu", "history")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
-	view.Set("hasSaveEntry", c.store.HasSaveEntry(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
+	view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
 
 	html.OK(w, r, view.Render("entry"))
 }
diff --git a/ui/entry_save.go b/ui/entry_save.go
index 93910c07..86d5b4d9 100644
--- a/ui/entry_save.go
+++ b/ui/entry_save.go
@@ -13,10 +13,9 @@ import (
 	"miniflux.app/model"
 )
 
-// SaveEntry send the link to external services.
-func (c *Controller) SaveEntry(w http.ResponseWriter, r *http.Request) {
+func (h *handler) saveEntry(w http.ResponseWriter, r *http.Request) {
 	entryID := request.RouteInt64Param(r, "entryID")
-	builder := c.store.NewEntryQueryBuilder(request.UserID(r))
+	builder := h.store.NewEntryQueryBuilder(request.UserID(r))
 	builder.WithEntryID(entryID)
 	builder.WithoutStatus(model.EntryStatusRemoved)
 
@@ -31,14 +30,14 @@ func (c *Controller) SaveEntry(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	settings, err := c.store.Integration(request.UserID(r))
+	settings, err := h.store.Integration(request.UserID(r))
 	if err != nil {
 		json.ServerError(w, r, err)
 		return
 	}
 
 	go func() {
-		integration.SendEntry(c.cfg, entry, settings)
+		integration.SendEntry(h.cfg, entry, settings)
 	}()
 
 	json.Created(w, r, map[string]string{"message": "saved"})
diff --git a/ui/entry_scraper.go b/ui/entry_scraper.go
index c35d9497..519bfd29 100644
--- a/ui/entry_scraper.go
+++ b/ui/entry_scraper.go
@@ -14,10 +14,9 @@ import (
 	"miniflux.app/reader/scraper"
 )
 
-// FetchContent downloads the original HTML page and returns relevant contents.
-func (c *Controller) FetchContent(w http.ResponseWriter, r *http.Request) {
+func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
 	entryID := request.RouteInt64Param(r, "entryID")
-	builder := c.store.NewEntryQueryBuilder(request.UserID(r))
+	builder := h.store.NewEntryQueryBuilder(request.UserID(r))
 	builder.WithEntryID(entryID)
 	builder.WithoutStatus(model.EntryStatusRemoved)
 
@@ -39,7 +38,7 @@ func (c *Controller) FetchContent(w http.ResponseWriter, r *http.Request) {
 	}
 
 	entry.Content = sanitizer.Sanitize(entry.URL, content)
-	c.store.UpdateEntryContent(entry)
+	h.store.UpdateEntryContent(entry)
 
 	json.OK(w, r, map[string]string{"content": entry.Content})
 }
diff --git a/ui/entry_search.go b/ui/entry_search.go
index d083b353..252b3065 100644
--- a/ui/entry_search.go
+++ b/ui/entry_search.go
@@ -17,9 +17,8 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ShowSearchEntry shows a single entry in "search" mode.
-func (c *Controller) ShowSearchEntry(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showSearchEntryPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -27,7 +26,7 @@ func (c *Controller) ShowSearchEntry(w http.ResponseWriter, r *http.Request) {
 
 	entryID := request.RouteInt64Param(r, "entryID")
 	searchQuery := request.QueryStringParam(r, "q", "")
-	builder := c.store.NewEntryQueryBuilder(user.ID)
+	builder := h.store.NewEntryQueryBuilder(user.ID)
 	builder.WithSearchQuery(searchQuery)
 	builder.WithEntryID(entryID)
 	builder.WithoutStatus(model.EntryStatusRemoved)
@@ -44,7 +43,7 @@ func (c *Controller) ShowSearchEntry(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if entry.Status == model.EntryStatusUnread {
-		err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+		err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
 		if err != nil {
 			logger.Error("[Controller:ShowSearchEntry] %v", err)
 			html.ServerError(w, r, err)
@@ -54,7 +53,7 @@ func (c *Controller) ShowSearchEntry(w http.ResponseWriter, r *http.Request) {
 		entry.Status = model.EntryStatusRead
 	}
 
-	entryPaginationBuilder := storage.NewEntryPaginationBuilder(c.store, user.ID, entry.ID, user.EntryDirection)
+	entryPaginationBuilder := storage.NewEntryPaginationBuilder(h.store, user.ID, entry.ID, user.EntryDirection)
 	entryPaginationBuilder.WithSearchQuery(searchQuery)
 	prevEntry, nextEntry, err := entryPaginationBuilder.Entries()
 	if err != nil {
@@ -64,16 +63,16 @@ func (c *Controller) ShowSearchEntry(w http.ResponseWriter, r *http.Request) {
 
 	nextEntryRoute := ""
 	if nextEntry != nil {
-		nextEntryRoute = route.Path(c.router, "searchEntry", "entryID", nextEntry.ID)
+		nextEntryRoute = route.Path(h.router, "searchEntry", "entryID", nextEntry.ID)
 	}
 
 	prevEntryRoute := ""
 	if prevEntry != nil {
-		prevEntryRoute = route.Path(c.router, "searchEntry", "entryID", prevEntry.ID)
+		prevEntryRoute = route.Path(h.router, "searchEntry", "entryID", prevEntry.ID)
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("searchQuery", searchQuery)
 	view.Set("entry", entry)
 	view.Set("prevEntry", prevEntry)
@@ -82,9 +81,9 @@ func (c *Controller) ShowSearchEntry(w http.ResponseWriter, r *http.Request) {
 	view.Set("prevEntryRoute", prevEntryRoute)
 	view.Set("menu", "search")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
-	view.Set("hasSaveEntry", c.store.HasSaveEntry(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
+	view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
 
 	html.OK(w, r, view.Render("entry"))
 }
diff --git a/ui/entry_toggle_bookmark.go b/ui/entry_toggle_bookmark.go
index 9f7a3e77..3e638a11 100644
--- a/ui/entry_toggle_bookmark.go
+++ b/ui/entry_toggle_bookmark.go
@@ -11,10 +11,9 @@ import (
 	"miniflux.app/http/response/json"
 )
 
-// ToggleBookmark handles Ajax request to toggle bookmark value.
-func (c *Controller) ToggleBookmark(w http.ResponseWriter, r *http.Request) {
+func (h *handler) toggleBookmark(w http.ResponseWriter, r *http.Request) {
 	entryID := request.RouteInt64Param(r, "entryID")
-	if err := c.store.ToggleBookmark(request.UserID(r), entryID); err != nil {
+	if err := h.store.ToggleBookmark(request.UserID(r), entryID); err != nil {
 		json.ServerError(w, r, err)
 		return
 	}
diff --git a/ui/entry_unread.go b/ui/entry_unread.go
index 37e9592e..fb8d45ce 100644
--- a/ui/entry_unread.go
+++ b/ui/entry_unread.go
@@ -16,16 +16,15 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ShowUnreadEntry shows a single feed entry in "unread" mode.
-func (c *Controller) ShowUnreadEntry(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showUnreadEntryPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	entryID := request.RouteInt64Param(r, "entryID")
-	builder := c.store.NewEntryQueryBuilder(user.ID)
+	builder := h.store.NewEntryQueryBuilder(user.ID)
 	builder.WithEntryID(entryID)
 	builder.WithoutStatus(model.EntryStatusRemoved)
 
@@ -42,14 +41,14 @@ func (c *Controller) ShowUnreadEntry(w http.ResponseWriter, r *http.Request) {
 
 	// Make sure we always get the pagination in unread mode even if the page is refreshed.
 	if entry.Status == model.EntryStatusRead {
-		err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusUnread)
+		err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusUnread)
 		if err != nil {
 			html.ServerError(w, r, err)
 			return
 		}
 	}
 
-	entryPaginationBuilder := storage.NewEntryPaginationBuilder(c.store, user.ID, entry.ID, user.EntryDirection)
+	entryPaginationBuilder := storage.NewEntryPaginationBuilder(h.store, user.ID, entry.ID, user.EntryDirection)
 	entryPaginationBuilder.WithStatus(model.EntryStatusUnread)
 	prevEntry, nextEntry, err := entryPaginationBuilder.Entries()
 	if err != nil {
@@ -59,24 +58,24 @@ func (c *Controller) ShowUnreadEntry(w http.ResponseWriter, r *http.Request) {
 
 	nextEntryRoute := ""
 	if nextEntry != nil {
-		nextEntryRoute = route.Path(c.router, "unreadEntry", "entryID", nextEntry.ID)
+		nextEntryRoute = route.Path(h.router, "unreadEntry", "entryID", nextEntry.ID)
 	}
 
 	prevEntryRoute := ""
 	if prevEntry != nil {
-		prevEntryRoute = route.Path(c.router, "unreadEntry", "entryID", prevEntry.ID)
+		prevEntryRoute = route.Path(h.router, "unreadEntry", "entryID", prevEntry.ID)
 	}
 
 	// Always mark the entry as read after fetching the pagination.
-	err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+	err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 	entry.Status = model.EntryStatusRead
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("entry", entry)
 	view.Set("prevEntry", prevEntry)
 	view.Set("nextEntry", nextEntry)
@@ -84,11 +83,11 @@ func (c *Controller) ShowUnreadEntry(w http.ResponseWriter, r *http.Request) {
 	view.Set("prevEntryRoute", prevEntryRoute)
 	view.Set("menu", "unread")
 	view.Set("user", user)
-	view.Set("hasSaveEntry", c.store.HasSaveEntry(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 
 	// Fetching the counter here avoid to be off by one.
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
 
 	html.OK(w, r, view.Render("entry"))
 }
diff --git a/ui/entry_update_status.go b/ui/entry_update_status.go
index 6c5cb127..ac1c284a 100644
--- a/ui/entry_update_status.go
+++ b/ui/entry_update_status.go
@@ -12,8 +12,7 @@ import (
 	"miniflux.app/http/response/json"
 )
 
-// UpdateEntriesStatus updates the status for a list of entries.
-func (c *Controller) UpdateEntriesStatus(w http.ResponseWriter, r *http.Request) {
+func (h *handler) updateEntriesStatus(w http.ResponseWriter, r *http.Request) {
 	entryIDs, status, err := decodeEntryStatusPayload(r.Body)
 	if err != nil {
 		json.BadRequest(w, r, err)
@@ -25,7 +24,7 @@ func (c *Controller) UpdateEntriesStatus(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	err = c.store.SetEntriesStatus(request.UserID(r), entryIDs, status)
+	err = h.store.SetEntriesStatus(request.UserID(r), entryIDs, status)
 	if err != nil {
 		json.ServerError(w, r, err)
 		return
diff --git a/ui/feed_edit.go b/ui/feed_edit.go
index 330817bf..a1956132 100644
--- a/ui/feed_edit.go
+++ b/ui/feed_edit.go
@@ -15,16 +15,15 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// EditFeed shows the form to modify a subscription.
-func (c *Controller) EditFeed(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showEditFeedPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	feedID := request.RouteInt64Param(r, "feedID")
-	feed, err := c.store.FeedByID(user.ID, feedID)
+	feed, err := h.store.FeedByID(user.ID, feedID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -35,7 +34,7 @@ func (c *Controller) EditFeed(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	categories, err := c.store.Categories(user.ID)
+	categories, err := h.store.Categories(user.ID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -54,15 +53,15 @@ func (c *Controller) EditFeed(w http.ResponseWriter, r *http.Request) {
 		Password:     feed.Password,
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("form", feedForm)
 	view.Set("categories", categories)
 	view.Set("feed", feed)
 	view.Set("menu", "feeds")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 	view.Set("defaultUserAgent", client.DefaultUserAgent)
 
 	html.OK(w, r, view.Render("edit_feed"))
diff --git a/ui/feed_entries.go b/ui/feed_entries.go
index 685b5c16..46fec9a2 100644
--- a/ui/feed_entries.go
+++ b/ui/feed_entries.go
@@ -15,16 +15,15 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ShowFeedEntries shows all entries for the given feed.
-func (c *Controller) ShowFeedEntries(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showFeedEntriesPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	feedID := request.RouteInt64Param(r, "feedID")
-	feed, err := c.store.FeedByID(user.ID, feedID)
+	feed, err := h.store.FeedByID(user.ID, feedID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -36,7 +35,7 @@ func (c *Controller) ShowFeedEntries(w http.ResponseWriter, r *http.Request) {
 	}
 
 	offset := request.QueryIntParam(r, "offset", 0)
-	builder := c.store.NewEntryQueryBuilder(user.ID)
+	builder := h.store.NewEntryQueryBuilder(user.ID)
 	builder.WithFeedID(feed.ID)
 	builder.WithoutStatus(model.EntryStatusRemoved)
 	builder.WithOrder(model.DefaultSortingOrder)
@@ -56,17 +55,17 @@ func (c *Controller) ShowFeedEntries(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("feed", feed)
 	view.Set("entries", entries)
 	view.Set("total", count)
-	view.Set("pagination", c.getPagination(route.Path(c.router, "feedEntries", "feedID", feed.ID), count, offset))
+	view.Set("pagination", getPagination(route.Path(h.router, "feedEntries", "feedID", feed.ID), count, offset))
 	view.Set("menu", "feeds")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
-	view.Set("hasSaveEntry", c.store.HasSaveEntry(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
+	view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
 
 	html.OK(w, r, view.Render("feed_entries"))
 }
diff --git a/ui/feed_icon.go b/ui/feed_icon.go
index 63aa050f..4b5f30f7 100644
--- a/ui/feed_icon.go
+++ b/ui/feed_icon.go
@@ -13,10 +13,9 @@ import (
 	"miniflux.app/http/response/html"
 )
 
-// ShowIcon shows the feed icon.
-func (c *Controller) ShowIcon(w http.ResponseWriter, r *http.Request) {
+func (h *handler) showIcon(w http.ResponseWriter, r *http.Request) {
 	iconID := request.RouteInt64Param(r, "iconID")
-	icon, err := c.store.IconByID(iconID)
+	icon, err := h.store.IconByID(iconID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
diff --git a/ui/feed_list.go b/ui/feed_list.go
index ac5a97c3..0c2c5465 100644
--- a/ui/feed_list.go
+++ b/ui/feed_list.go
@@ -13,28 +13,27 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ShowFeedsPage shows the page with all subscriptions.
-func (c *Controller) ShowFeedsPage(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showFeedsPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	feeds, err := c.store.Feeds(user.ID)
+	feeds, err := h.store.Feeds(user.ID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("feeds", feeds)
 	view.Set("total", len(feeds))
 	view.Set("menu", "feeds")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 
 	html.OK(w, r, view.Render("feeds"))
 }
diff --git a/ui/feed_refresh.go b/ui/feed_refresh.go
index 0da4eb8e..1b9dc3c9 100644
--- a/ui/feed_refresh.go
+++ b/ui/feed_refresh.go
@@ -13,28 +13,26 @@ import (
 	"miniflux.app/logger"
 )
 
-// RefreshFeed refresh a subscription and redirect to the feed entries page.
-func (c *Controller) RefreshFeed(w http.ResponseWriter, r *http.Request) {
+func (h *handler) refreshFeed(w http.ResponseWriter, r *http.Request) {
 	feedID := request.RouteInt64Param(r, "feedID")
-	if err := c.feedHandler.RefreshFeed(request.UserID(r), feedID); err != nil {
+	if err := h.feedHandler.RefreshFeed(request.UserID(r), feedID); err != nil {
 		logger.Error("[Controller:RefreshFeed] %v", err)
 	}
 
-	html.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feedID))
+	html.Redirect(w, r, route.Path(h.router, "feedEntries", "feedID", feedID))
 }
 
-// RefreshAllFeeds refresh all feeds in the background for the current user.
-func (c *Controller) RefreshAllFeeds(w http.ResponseWriter, r *http.Request) {
+func (h *handler) refreshAllFeeds(w http.ResponseWriter, r *http.Request) {
 	userID := request.UserID(r)
-	jobs, err := c.store.NewUserBatch(userID, c.store.CountFeeds(userID))
+	jobs, err := h.store.NewUserBatch(userID, h.store.CountFeeds(userID))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	go func() {
-		c.pool.Push(jobs)
+		h.pool.Push(jobs)
 	}()
 
-	html.Redirect(w, r, route.Path(c.router, "feeds"))
+	html.Redirect(w, r, route.Path(h.router, "feeds"))
 }
diff --git a/ui/feed_remove.go b/ui/feed_remove.go
index 41fc5b08..c70d77a2 100644
--- a/ui/feed_remove.go
+++ b/ui/feed_remove.go
@@ -12,13 +12,12 @@ import (
 	"miniflux.app/http/route"
 )
 
-// RemoveFeed deletes a subscription from the database and redirect to the list of feeds page.
-func (c *Controller) RemoveFeed(w http.ResponseWriter, r *http.Request) {
+func (h *handler) removeFeed(w http.ResponseWriter, r *http.Request) {
 	feedID := request.RouteInt64Param(r, "feedID")
-	if err := c.store.RemoveFeed(request.UserID(r), feedID); err != nil {
+	if err := h.store.RemoveFeed(request.UserID(r), feedID); err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	html.Redirect(w, r, route.Path(c.router, "feeds"))
+	html.Redirect(w, r, route.Path(h.router, "feeds"))
 }
diff --git a/ui/feed_update.go b/ui/feed_update.go
index 66b6d40e..936fd1fb 100644
--- a/ui/feed_update.go
+++ b/ui/feed_update.go
@@ -17,16 +17,15 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// UpdateFeed update a subscription and redirect to the feed entries page.
-func (c *Controller) UpdateFeed(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	feedID := request.RouteInt64Param(r, "feedID")
-	feed, err := c.store.FeedByID(user.ID, feedID)
+	feed, err := h.store.FeedByID(user.ID, feedID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -37,7 +36,7 @@ func (c *Controller) UpdateFeed(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	categories, err := c.store.Categories(user.ID)
+	categories, err := h.store.Categories(user.ID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -45,15 +44,15 @@ func (c *Controller) UpdateFeed(w http.ResponseWriter, r *http.Request) {
 
 	feedForm := form.NewFeedForm(r)
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("form", feedForm)
 	view.Set("categories", categories)
 	view.Set("feed", feed)
 	view.Set("menu", "feeds")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 	view.Set("defaultUserAgent", client.DefaultUserAgent)
 
 	if err := feedForm.ValidateModification(); err != nil {
@@ -62,7 +61,7 @@ func (c *Controller) UpdateFeed(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	err = c.store.UpdateFeed(feedForm.Merge(feed))
+	err = h.store.UpdateFeed(feedForm.Merge(feed))
 	if err != nil {
 		logger.Error("[Controller:EditFeed] %v", err)
 		view.Set("errorMessage", "error.unable_to_update_feed")
@@ -70,5 +69,5 @@ func (c *Controller) UpdateFeed(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	html.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feed.ID))
+	html.Redirect(w, r, route.Path(h.router, "feedEntries", "feedID", feed.ID))
 }
diff --git a/ui/controller.go b/ui/handler.go
similarity index 54%
rename from ui/controller.go
rename to ui/handler.go
index b08c253e..39fe560a 100644
--- a/ui/controller.go
+++ b/ui/handler.go
@@ -14,24 +14,11 @@ import (
 	"github.com/gorilla/mux"
 )
 
-// Controller contains all HTTP handlers for the user interface.
-type Controller struct {
+type handler struct {
+	router      *mux.Router
 	cfg         *config.Config
 	store       *storage.Storage
+	tpl         *template.Engine
 	pool        *scheduler.WorkerPool
 	feedHandler *feed.Handler
-	tpl         *template.Engine
-	router      *mux.Router
-}
-
-// NewController returns a new Controller.
-func NewController(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler, tpl *template.Engine, router *mux.Router) *Controller {
-	return &Controller{
-		cfg:         cfg,
-		store:       store,
-		pool:        pool,
-		feedHandler: feedHandler,
-		tpl:         tpl,
-		router:      router,
-	}
 }
diff --git a/ui/history_entries.go b/ui/history_entries.go
index 8449240a..131879c8 100644
--- a/ui/history_entries.go
+++ b/ui/history_entries.go
@@ -15,16 +15,15 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ShowHistoryPage renders the page with all read entries.
-func (c *Controller) ShowHistoryPage(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showHistoryPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	offset := request.QueryIntParam(r, "offset", 0)
-	builder := c.store.NewEntryQueryBuilder(user.ID)
+	builder := h.store.NewEntryQueryBuilder(user.ID)
 	builder.WithStatus(model.EntryStatusRead)
 	builder.WithOrder(model.DefaultSortingOrder)
 	builder.WithDirection(user.EntryDirection)
@@ -43,16 +42,16 @@ func (c *Controller) ShowHistoryPage(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("entries", entries)
 	view.Set("total", count)
-	view.Set("pagination", c.getPagination(route.Path(c.router, "history"), count, offset))
+	view.Set("pagination", getPagination(route.Path(h.router, "history"), count, offset))
 	view.Set("menu", "history")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
-	view.Set("hasSaveEntry", c.store.HasSaveEntry(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
+	view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
 
 	html.OK(w, r, view.Render("history_entries"))
 }
diff --git a/ui/history_flush.go b/ui/history_flush.go
index 6c6907b8..d0b8f628 100644
--- a/ui/history_flush.go
+++ b/ui/history_flush.go
@@ -12,13 +12,12 @@ import (
 	"miniflux.app/http/route"
 )
 
-// FlushHistory changes all "read" items to "removed".
-func (c *Controller) FlushHistory(w http.ResponseWriter, r *http.Request) {
-	err := c.store.FlushHistory(request.UserID(r))
+func (h *handler) flushHistory(w http.ResponseWriter, r *http.Request) {
+	err := h.store.FlushHistory(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	html.Redirect(w, r, route.Path(c.router, "history"))
+	html.Redirect(w, r, route.Path(h.router, "history"))
 }
diff --git a/ui/integration_pocket.go b/ui/integration_pocket.go
index 47a975c7..8f8c680c 100644
--- a/ui/integration_pocket.go
+++ b/ui/integration_pocket.go
@@ -16,29 +16,28 @@ import (
 	"miniflux.app/ui/session"
 )
 
-// PocketAuthorize redirects the end-user to Pocket website to authorize the application.
-func (c *Controller) PocketAuthorize(w http.ResponseWriter, r *http.Request) {
+func (h *handler) pocketAuthorize(w http.ResponseWriter, r *http.Request) {
 	printer := locale.NewPrinter(request.UserLanguage(r))
-	user, err := c.store.UserByID(request.UserID(r))
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	integration, err := c.store.Integration(user.ID)
+	integration, err := h.store.Integration(user.ID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	connector := pocket.NewConnector(c.cfg.PocketConsumerKey(integration.PocketConsumerKey))
-	redirectURL := c.cfg.BaseURL() + route.Path(c.router, "pocketCallback")
+	sess := session.New(h.store, request.SessionID(r))
+	connector := pocket.NewConnector(h.cfg.PocketConsumerKey(integration.PocketConsumerKey))
+	redirectURL := h.cfg.BaseURL() + route.Path(h.router, "pocketCallback")
 	requestToken, err := connector.RequestToken(redirectURL)
 	if err != nil {
 		logger.Error("[Pocket:Authorize] %v", err)
 		sess.NewFlashErrorMessage(printer.Printf("error.pocket_request_token"))
-		html.Redirect(w, r, route.Path(c.router, "integrations"))
+		html.Redirect(w, r, route.Path(h.router, "integrations"))
 		return
 	}
 
@@ -46,41 +45,40 @@ func (c *Controller) PocketAuthorize(w http.ResponseWriter, r *http.Request) {
 	html.Redirect(w, r, connector.AuthorizationURL(requestToken, redirectURL))
 }
 
-// PocketCallback saves the personal access token after the authorization step.
-func (c *Controller) PocketCallback(w http.ResponseWriter, r *http.Request) {
+func (h *handler) pocketCallback(w http.ResponseWriter, r *http.Request) {
 	printer := locale.NewPrinter(request.UserLanguage(r))
-	sess := session.New(c.store, request.SessionID(r))
+	sess := session.New(h.store, request.SessionID(r))
 
-	user, err := c.store.UserByID(request.UserID(r))
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	integration, err := c.store.Integration(user.ID)
+	integration, err := h.store.Integration(user.ID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	connector := pocket.NewConnector(c.cfg.PocketConsumerKey(integration.PocketConsumerKey))
+	connector := pocket.NewConnector(h.cfg.PocketConsumerKey(integration.PocketConsumerKey))
 	accessToken, err := connector.AccessToken(request.PocketRequestToken(r))
 	if err != nil {
 		logger.Error("[Pocket:Callback] %v", err)
 		sess.NewFlashErrorMessage(printer.Printf("error.pocket_access_token"))
-		html.Redirect(w, r, route.Path(c.router, "integrations"))
+		html.Redirect(w, r, route.Path(h.router, "integrations"))
 		return
 	}
 
 	sess.SetPocketRequestToken("")
 	integration.PocketAccessToken = accessToken
 
-	err = c.store.UpdateIntegration(integration)
+	err = h.store.UpdateIntegration(integration)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	sess.NewFlashMessage(printer.Printf("alert.pocket_linked"))
-	html.Redirect(w, r, route.Path(c.router, "integrations"))
+	html.Redirect(w, r, route.Path(h.router, "integrations"))
 }
diff --git a/ui/integration_show.go b/ui/integration_show.go
index 3cea8305..c7f27406 100644
--- a/ui/integration_show.go
+++ b/ui/integration_show.go
@@ -14,15 +14,14 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ShowIntegrations renders the page with all external integrations.
-func (c *Controller) ShowIntegrations(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	integration, err := c.store.Integration(user.ID)
+	integration, err := h.store.Integration(user.ID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -53,14 +52,14 @@ func (c *Controller) ShowIntegrations(w http.ResponseWriter, r *http.Request) {
 		PocketConsumerKey:    integration.PocketConsumerKey,
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("form", integrationForm)
 	view.Set("menu", "settings")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
-	view.Set("hasPocketConsumerKeyConfigured", c.cfg.PocketConsumerKey("") != "")
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
+	view.Set("hasPocketConsumerKeyConfigured", h.cfg.PocketConsumerKey("") != "")
 
 	html.OK(w, r, view.Render("integrations"))
 }
diff --git a/ui/integration_update.go b/ui/integration_update.go
index 5661daff..16fdb0f3 100644
--- a/ui/integration_update.go
+++ b/ui/integration_update.go
@@ -17,17 +17,16 @@ import (
 	"miniflux.app/ui/session"
 )
 
-// UpdateIntegration updates integration settings.
-func (c *Controller) UpdateIntegration(w http.ResponseWriter, r *http.Request) {
+func (h *handler) updateIntegration(w http.ResponseWriter, r *http.Request) {
 	printer := locale.NewPrinter(request.UserLanguage(r))
-	sess := session.New(c.store, request.SessionID(r))
-	user, err := c.store.UserByID(request.UserID(r))
+	sess := session.New(h.store, request.SessionID(r))
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	integration, err := c.store.Integration(user.ID)
+	integration, err := h.store.Integration(user.ID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -36,9 +35,9 @@ func (c *Controller) UpdateIntegration(w http.ResponseWriter, r *http.Request) {
 	integrationForm := form.NewIntegrationForm(r)
 	integrationForm.Merge(integration)
 
-	if integration.FeverUsername != "" && c.store.HasDuplicateFeverUsername(user.ID, integration.FeverUsername) {
+	if integration.FeverUsername != "" && h.store.HasDuplicateFeverUsername(user.ID, integration.FeverUsername) {
 		sess.NewFlashErrorMessage(printer.Printf("error.duplicate_fever_username"))
-		html.Redirect(w, r, route.Path(c.router, "integrations"))
+		html.Redirect(w, r, route.Path(h.router, "integrations"))
 		return
 	}
 
@@ -48,12 +47,12 @@ func (c *Controller) UpdateIntegration(w http.ResponseWriter, r *http.Request) {
 		integration.FeverToken = ""
 	}
 
-	err = c.store.UpdateIntegration(integration)
+	err = h.store.UpdateIntegration(integration)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	sess.NewFlashMessage(printer.Printf("alert.prefs_saved"))
-	html.Redirect(w, r, route.Path(c.router, "integrations"))
+	html.Redirect(w, r, route.Path(h.router, "integrations"))
 }
diff --git a/ui/login_check.go b/ui/login_check.go
index 73a17362..acc762a0 100644
--- a/ui/login_check.go
+++ b/ui/login_check.go
@@ -13,13 +13,12 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// CheckLogin validates the username/password and redirects the user to the unread page.
-func (c *Controller) CheckLogin(w http.ResponseWriter, r *http.Request) {
+func (h *handler) checkLogin(w http.ResponseWriter, r *http.Request) {
 	clientIP := request.ClientIP(r)
-	sess := session.New(c.store, request.SessionID(r))
+	sess := session.New(h.store, request.SessionID(r))
 	authForm := form.NewAuthForm(r)
 
-	view := view.New(c.tpl, r, sess)
+	view := view.New(h.tpl, r, sess)
 	view.Set("errorMessage", "error.bad_credentials")
 	view.Set("form", authForm)
 
@@ -29,22 +28,22 @@ func (c *Controller) CheckLogin(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if err := c.store.CheckPassword(authForm.Username, authForm.Password); err != nil {
+	if err := h.store.CheckPassword(authForm.Username, authForm.Password); err != nil {
 		logger.Error("[Controller:CheckLogin] [ClientIP=%s] %v", clientIP, err)
 		html.OK(w, r, view.Render("login"))
 		return
 	}
 
-	sessionToken, userID, err := c.store.CreateUserSession(authForm.Username, r.UserAgent(), clientIP)
+	sessionToken, userID, err := h.store.CreateUserSession(authForm.Username, r.UserAgent(), clientIP)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	logger.Info("[Controller:CheckLogin] username=%s just logged in", authForm.Username)
-	c.store.SetLastLogin(userID)
+	h.store.SetLastLogin(userID)
 
-	user, err := c.store.UserByID(userID)
+	user, err := h.store.UserByID(userID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -56,9 +55,9 @@ func (c *Controller) CheckLogin(w http.ResponseWriter, r *http.Request) {
 	http.SetCookie(w, cookie.New(
 		cookie.CookieUserSessionID,
 		sessionToken,
-		c.cfg.IsHTTPS,
-		c.cfg.BasePath(),
+		h.cfg.IsHTTPS,
+		h.cfg.BasePath(),
 	))
 
-	html.Redirect(w, r, route.Path(c.router, "unread"))
+	html.Redirect(w, r, route.Path(h.router, "unread"))
 }
diff --git a/ui/login_show.go b/ui/login_show.go
index 890f9db6..ea458f47 100644
--- a/ui/login_show.go
+++ b/ui/login_show.go
@@ -14,14 +14,13 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ShowLoginPage shows the login form.
-func (c *Controller) ShowLoginPage(w http.ResponseWriter, r *http.Request) {
+func (h *handler) showLoginPage(w http.ResponseWriter, r *http.Request) {
 	if request.IsAuthenticated(r) {
-		html.Redirect(w, r, route.Path(c.router, "unread"))
+		html.Redirect(w, r, route.Path(h.router, "unread"))
 		return
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	html.OK(w, r, view.Render("login"))
 }
diff --git a/ui/logout.go b/ui/logout.go
index 59f8af2d..ecb08b9b 100644
--- a/ui/logout.go
+++ b/ui/logout.go
@@ -15,10 +15,9 @@ import (
 	"miniflux.app/ui/session"
 )
 
-// Logout destroy the session and redirects the user to the login page.
-func (c *Controller) Logout(w http.ResponseWriter, r *http.Request) {
-	sess := session.New(c.store, request.SessionID(r))
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) logout(w http.ResponseWriter, r *http.Request) {
+	sess := session.New(h.store, request.SessionID(r))
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -27,15 +26,15 @@ func (c *Controller) Logout(w http.ResponseWriter, r *http.Request) {
 	sess.SetLanguage(user.Language)
 	sess.SetTheme(user.Theme)
 
-	if err := c.store.RemoveUserSessionByToken(user.ID, request.UserSessionToken(r)); err != nil {
+	if err := h.store.RemoveUserSessionByToken(user.ID, request.UserSessionToken(r)); err != nil {
 		logger.Error("[Controller:Logout] %v", err)
 	}
 
 	http.SetCookie(w, cookie.Expired(
 		cookie.CookieUserSessionID,
-		c.cfg.IsHTTPS,
-		c.cfg.BasePath(),
+		h.cfg.IsHTTPS,
+		h.cfg.BasePath(),
 	))
 
-	html.Redirect(w, r, route.Path(c.router, "login"))
+	html.Redirect(w, r, route.Path(h.router, "login"))
 }
diff --git a/ui/middleware.go b/ui/middleware.go
new file mode 100644
index 00000000..6ec68eeb
--- /dev/null
+++ b/ui/middleware.go
@@ -0,0 +1,149 @@
+// 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 ui // import "miniflux.app/ui"
+
+import (
+	"context"
+	"errors"
+	"net/http"
+
+	"miniflux.app/config"
+	"miniflux.app/http/cookie"
+	"miniflux.app/http/request"
+	"miniflux.app/http/response/html"
+	"miniflux.app/http/route"
+	"miniflux.app/storage"
+	"miniflux.app/logger"
+	"miniflux.app/model"
+
+	"github.com/gorilla/mux"
+)
+
+type middleware struct {
+	router *mux.Router
+	cfg *config.Config
+	store *storage.Storage
+}
+
+func newMiddleware(router *mux.Router, cfg *config.Config, store *storage.Storage) *middleware {
+	return &middleware{router, cfg, store}
+}
+
+func (m *middleware) handleUserSession(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		session := m.getUserSessionFromCookie(r)
+
+		if session == nil {
+			logger.Debug("[UserSession] Session not found")
+			if m.isPublicRoute(r) {
+				next.ServeHTTP(w, r)
+			} else {
+				html.Redirect(w, r, route.Path(m.router, "login"))
+			}
+		} else {
+			logger.Debug("[UserSession] %s", session)
+
+			ctx := r.Context()
+			ctx = context.WithValue(ctx, request.UserIDContextKey, session.UserID)
+			ctx = context.WithValue(ctx, request.IsAuthenticatedContextKey, true)
+			ctx = context.WithValue(ctx, request.UserSessionTokenContextKey, session.Token)
+
+			next.ServeHTTP(w, r.WithContext(ctx))
+		}
+	})
+}
+
+func (m *middleware) handleAppSession(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		var err error
+		session := m.getAppSessionValueFromCookie(r)
+
+		if session == nil {
+			logger.Debug("[AppSession] Session not found")
+
+			session, err = m.store.CreateSession()
+			if err != nil {
+				html.ServerError(w, r, err)
+				return
+			}
+
+			http.SetCookie(w, cookie.New(cookie.CookieSessionID, session.ID, m.cfg.IsHTTPS, m.cfg.BasePath()))
+		} else {
+			logger.Debug("[AppSession] %s", session)
+		}
+
+		if r.Method == "POST" {
+			formValue := r.FormValue("csrf")
+			headerValue := r.Header.Get("X-Csrf-Token")
+
+			if session.Data.CSRF != formValue && session.Data.CSRF != headerValue {
+				logger.Error(`[AppSession] Invalid or missing CSRF token: Form="%s", Header="%s"`, formValue, headerValue)
+				html.BadRequest(w, r, errors.New("Invalid or missing CSRF"))
+				return
+			}
+		}
+
+		ctx := r.Context()
+		ctx = context.WithValue(ctx, request.SessionIDContextKey, session.ID)
+		ctx = context.WithValue(ctx, request.CSRFContextKey, session.Data.CSRF)
+		ctx = context.WithValue(ctx, request.OAuth2StateContextKey, session.Data.OAuth2State)
+		ctx = context.WithValue(ctx, request.FlashMessageContextKey, session.Data.FlashMessage)
+		ctx = context.WithValue(ctx, request.FlashErrorMessageContextKey, session.Data.FlashErrorMessage)
+		ctx = context.WithValue(ctx, request.UserLanguageContextKey, session.Data.Language)
+		ctx = context.WithValue(ctx, request.UserThemeContextKey, session.Data.Theme)
+		ctx = context.WithValue(ctx, request.PocketRequestTokenContextKey, session.Data.PocketRequestToken)
+		next.ServeHTTP(w, r.WithContext(ctx))
+	})
+}
+
+func (m *middleware) getAppSessionValueFromCookie(r *http.Request) *model.Session {
+	cookieValue := request.CookieValue(r, cookie.CookieSessionID)
+	if cookieValue == "" {
+		return nil
+	}
+
+	session, err := m.store.Session(cookieValue)
+	if err != nil {
+		logger.Error("[AppSession] %v", err)
+		return nil
+	}
+
+	return session
+}
+
+func (m *middleware) isPublicRoute(r *http.Request) bool {
+	route := mux.CurrentRoute(r)
+	switch route.GetName() {
+	case "login",
+		"checkLogin",
+		"stylesheet",
+		"javascript",
+		"oauth2Redirect",
+		"oauth2Callback",
+		"appIcon",
+		"favicon",
+		"webManifest",
+		"robots",
+		"healthcheck":
+		return true
+	default:
+		return false
+	}
+}
+
+func (m *middleware) getUserSessionFromCookie(r *http.Request) *model.UserSession {
+	cookieValue := request.CookieValue(r, cookie.CookieUserSessionID)
+	if cookieValue == "" {
+		return nil
+	}
+
+	session, err := m.store.UserSessionByToken(cookieValue)
+	if err != nil {
+		logger.Error("[UserSession] %v", err)
+		return nil
+	}
+
+	return session
+}
diff --git a/ui/oauth2_callback.go b/ui/oauth2_callback.go
index 0aecd1c0..bd7c999c 100644
--- a/ui/oauth2_callback.go
+++ b/ui/oauth2_callback.go
@@ -17,51 +17,50 @@ import (
 	"miniflux.app/ui/session"
 )
 
-// OAuth2Callback receives the authorization code and create a new session.
-func (c *Controller) OAuth2Callback(w http.ResponseWriter, r *http.Request) {
+func (h *handler) oauth2Callback(w http.ResponseWriter, r *http.Request) {
 	clientIP := request.ClientIP(r)
 	printer := locale.NewPrinter(request.UserLanguage(r))
-	sess := session.New(c.store, request.SessionID(r))
+	sess := session.New(h.store, request.SessionID(r))
 
 	provider := request.RouteStringParam(r, "provider")
 	if provider == "" {
 		logger.Error("[OAuth2] Invalid or missing provider")
-		html.Redirect(w, r, route.Path(c.router, "login"))
+		html.Redirect(w, r, route.Path(h.router, "login"))
 		return
 	}
 
 	code := request.QueryStringParam(r, "code", "")
 	if code == "" {
 		logger.Error("[OAuth2] No code received on callback")
-		html.Redirect(w, r, route.Path(c.router, "login"))
+		html.Redirect(w, r, route.Path(h.router, "login"))
 		return
 	}
 
 	state := request.QueryStringParam(r, "state", "")
 	if state == "" || state != request.OAuth2State(r) {
 		logger.Error(`[OAuth2] Invalid state value: got "%s" instead of "%s"`, state, request.OAuth2State(r))
-		html.Redirect(w, r, route.Path(c.router, "login"))
+		html.Redirect(w, r, route.Path(h.router, "login"))
 		return
 	}
 
-	authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
+	authProvider, err := getOAuth2Manager(h.cfg).Provider(provider)
 	if err != nil {
 		logger.Error("[OAuth2] %v", err)
-		html.Redirect(w, r, route.Path(c.router, "login"))
+		html.Redirect(w, r, route.Path(h.router, "login"))
 		return
 	}
 
 	profile, err := authProvider.GetProfile(code)
 	if err != nil {
 		logger.Error("[OAuth2] %v", err)
-		html.Redirect(w, r, route.Path(c.router, "login"))
+		html.Redirect(w, r, route.Path(h.router, "login"))
 		return
 	}
 
 	logger.Info("[OAuth2] [ClientIP=%s] Successful auth for %s", clientIP, profile)
 
 	if request.IsAuthenticated(r) {
-		user, err := c.store.UserByExtraField(profile.Key, profile.ID)
+		user, err := h.store.UserByExtraField(profile.Key, profile.ID)
 		if err != nil {
 			html.ServerError(w, r, err)
 			return
@@ -70,28 +69,28 @@ func (c *Controller) OAuth2Callback(w http.ResponseWriter, r *http.Request) {
 		if user != nil {
 			logger.Error("[OAuth2] User #%d cannot be associated because %s is already associated", request.UserID(r), user.Username)
 			sess.NewFlashErrorMessage(printer.Printf("error.duplicate_linked_account"))
-			html.Redirect(w, r, route.Path(c.router, "settings"))
+			html.Redirect(w, r, route.Path(h.router, "settings"))
 			return
 		}
 
-		if err := c.store.UpdateExtraField(request.UserID(r), profile.Key, profile.ID); err != nil {
+		if err := h.store.UpdateExtraField(request.UserID(r), profile.Key, profile.ID); err != nil {
 			html.ServerError(w, r, err)
 			return
 		}
 
 		sess.NewFlashMessage(printer.Printf("alert.account_linked"))
-		html.Redirect(w, r, route.Path(c.router, "settings"))
+		html.Redirect(w, r, route.Path(h.router, "settings"))
 		return
 	}
 
-	user, err := c.store.UserByExtraField(profile.Key, profile.ID)
+	user, err := h.store.UserByExtraField(profile.Key, profile.ID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	if user == nil {
-		if !c.cfg.IsOAuth2UserCreationAllowed() {
+		if !h.cfg.IsOAuth2UserCreationAllowed() {
 			html.Forbidden(w, r)
 			return
 		}
@@ -101,13 +100,13 @@ func (c *Controller) OAuth2Callback(w http.ResponseWriter, r *http.Request) {
 		user.IsAdmin = false
 		user.Extra[profile.Key] = profile.ID
 
-		if err := c.store.CreateUser(user); err != nil {
+		if err := h.store.CreateUser(user); err != nil {
 			html.ServerError(w, r, err)
 			return
 		}
 	}
 
-	sessionToken, _, err := c.store.CreateUserSession(user.Username, r.UserAgent(), clientIP)
+	sessionToken, _, err := h.store.CreateUserSession(user.Username, r.UserAgent(), clientIP)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -115,16 +114,16 @@ func (c *Controller) OAuth2Callback(w http.ResponseWriter, r *http.Request) {
 
 	logger.Info("[OAuth2] [ClientIP=%s] username=%s (%s) just logged in", clientIP, user.Username, profile)
 
-	c.store.SetLastLogin(user.ID)
+	h.store.SetLastLogin(user.ID)
 	sess.SetLanguage(user.Language)
 	sess.SetTheme(user.Theme)
 
 	http.SetCookie(w, cookie.New(
 		cookie.CookieUserSessionID,
 		sessionToken,
-		c.cfg.IsHTTPS,
-		c.cfg.BasePath(),
+		h.cfg.IsHTTPS,
+		h.cfg.BasePath(),
 	))
 
-	html.Redirect(w, r, route.Path(c.router, "unread"))
+	html.Redirect(w, r, route.Path(h.router, "unread"))
 }
diff --git a/ui/oauth2_redirect.go b/ui/oauth2_redirect.go
index e54309ab..85116cff 100644
--- a/ui/oauth2_redirect.go
+++ b/ui/oauth2_redirect.go
@@ -14,21 +14,20 @@ import (
 	"miniflux.app/ui/session"
 )
 
-// OAuth2Redirect redirects the user to the consent page to ask for permission.
-func (c *Controller) OAuth2Redirect(w http.ResponseWriter, r *http.Request) {
-	sess := session.New(c.store, request.SessionID(r))
+func (h *handler) oauth2Redirect(w http.ResponseWriter, r *http.Request) {
+	sess := session.New(h.store, request.SessionID(r))
 
 	provider := request.RouteStringParam(r, "provider")
 	if provider == "" {
 		logger.Error("[OAuth2] Invalid or missing provider: %s", provider)
-		html.Redirect(w, r, route.Path(c.router, "login"))
+		html.Redirect(w, r, route.Path(h.router, "login"))
 		return
 	}
 
-	authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
+	authProvider, err := getOAuth2Manager(h.cfg).Provider(provider)
 	if err != nil {
 		logger.Error("[OAuth2] %v", err)
-		html.Redirect(w, r, route.Path(c.router, "login"))
+		html.Redirect(w, r, route.Path(h.router, "login"))
 		return
 	}
 
diff --git a/ui/oauth2_unlink.go b/ui/oauth2_unlink.go
index 8e38ddc6..3283f897 100644
--- a/ui/oauth2_unlink.go
+++ b/ui/oauth2_unlink.go
@@ -15,26 +15,25 @@ import (
 	"miniflux.app/ui/session"
 )
 
-// OAuth2Unlink unlink an account from the external provider.
-func (c *Controller) OAuth2Unlink(w http.ResponseWriter, r *http.Request) {
+func (h *handler) oauth2Unlink(w http.ResponseWriter, r *http.Request) {
 	printer := locale.NewPrinter(request.UserLanguage(r))
 	provider := request.RouteStringParam(r, "provider")
 	if provider == "" {
 		logger.Info("[OAuth2] Invalid or missing provider")
-		html.Redirect(w, r, route.Path(c.router, "login"))
+		html.Redirect(w, r, route.Path(h.router, "login"))
 		return
 	}
 
-	authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
+	authProvider, err := getOAuth2Manager(h.cfg).Provider(provider)
 	if err != nil {
 		logger.Error("[OAuth2] %v", err)
-		html.Redirect(w, r, route.Path(c.router, "settings"))
+		html.Redirect(w, r, route.Path(h.router, "settings"))
 		return
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
+	sess := session.New(h.store, request.SessionID(r))
 
-	hasPassword, err := c.store.HasPassword(request.UserID(r))
+	hasPassword, err := h.store.HasPassword(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -42,15 +41,15 @@ func (c *Controller) OAuth2Unlink(w http.ResponseWriter, r *http.Request) {
 
 	if !hasPassword {
 		sess.NewFlashErrorMessage(printer.Printf("error.unlink_account_without_password"))
-		html.Redirect(w, r, route.Path(c.router, "settings"))
+		html.Redirect(w, r, route.Path(h.router, "settings"))
 		return
 	}
 
-	if err := c.store.RemoveExtraField(request.UserID(r), authProvider.GetUserExtraKey()); err != nil {
+	if err := h.store.RemoveExtraField(request.UserID(r), authProvider.GetUserExtraKey()); err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	sess.NewFlashMessage(printer.Printf("alert.account_unlinked"))
-	html.Redirect(w, r, route.Path(c.router, "settings"))
+	html.Redirect(w, r, route.Path(h.router, "settings"))
 }
diff --git a/ui/opml_export.go b/ui/opml_export.go
index 55ceb561..4eb6d2c9 100644
--- a/ui/opml_export.go
+++ b/ui/opml_export.go
@@ -13,9 +13,8 @@ import (
 	"miniflux.app/reader/opml"
 )
 
-// Export generates the OPML file.
-func (c *Controller) Export(w http.ResponseWriter, r *http.Request) {
-	opml, err := opml.NewHandler(c.store).Export(request.UserID(r))
+func (h *handler) exportFeeds(w http.ResponseWriter, r *http.Request) {
+	opml, err := opml.NewHandler(h.store).Export(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
diff --git a/ui/opml_import.go b/ui/opml_import.go
index 9a763cae..06f54bb9 100644
--- a/ui/opml_import.go
+++ b/ui/opml_import.go
@@ -13,20 +13,19 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// Import shows the import form.
-func (c *Controller) Import(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showImportPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("menu", "feeds")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 
 	html.OK(w, r, view.Render("import"))
 }
diff --git a/ui/opml_upload.go b/ui/opml_upload.go
index 538f8226..82e2ac8a 100644
--- a/ui/opml_upload.go
+++ b/ui/opml_upload.go
@@ -16,9 +16,8 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// UploadOPML handles OPML file importation.
-func (c *Controller) UploadOPML(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) uploadOPML(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -27,7 +26,7 @@ func (c *Controller) UploadOPML(w http.ResponseWriter, r *http.Request) {
 	file, fileHeader, err := r.FormFile("file")
 	if err != nil {
 		logger.Error("[Controller:UploadOPML] %v", err)
-		html.Redirect(w, r, route.Path(c.router, "import"))
+		html.Redirect(w, r, route.Path(h.router, "import"))
 		return
 	}
 	defer file.Close()
@@ -39,12 +38,12 @@ func (c *Controller) UploadOPML(w http.ResponseWriter, r *http.Request) {
 		fileHeader.Size,
 	)
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("menu", "feeds")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 
 	if fileHeader.Size == 0 {
 		view.Set("errorMessage", "error.empty_file")
@@ -52,11 +51,11 @@ func (c *Controller) UploadOPML(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if impErr := opml.NewHandler(c.store).Import(user.ID, file); impErr != nil {
+	if impErr := opml.NewHandler(h.store).Import(user.ID, file); impErr != nil {
 		view.Set("errorMessage", impErr)
 		html.OK(w, r, view.Render("import"))
 		return
 	}
 
-	html.Redirect(w, r, route.Path(c.router, "feeds"))
+	html.Redirect(w, r, route.Path(h.router, "feeds"))
 }
diff --git a/ui/pagination.go b/ui/pagination.go
index 5fdf715a..7534431f 100644
--- a/ui/pagination.go
+++ b/ui/pagination.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 ui  // import "miniflux.app/ui"
+package ui // import "miniflux.app/ui"
 
 const (
 	nbItemsPerPage = 100
@@ -20,7 +20,7 @@ type pagination struct {
 	SearchQuery  string
 }
 
-func (c *Controller) getPagination(route string, total, offset int) pagination {
+func getPagination(route string, total, offset int) pagination {
 	nextOffset := 0
 	prevOffset := 0
 	showNext := (total - offset) > nbItemsPerPage
diff --git a/ui/proxy.go b/ui/proxy.go
index 050c0708..e9f8ec08 100644
--- a/ui/proxy.go
+++ b/ui/proxy.go
@@ -18,8 +18,7 @@ import (
 	"miniflux.app/http/response/html"
 )
 
-// ImageProxy fetch an image from a remote server and sent it back to the browser.
-func (c *Controller) ImageProxy(w http.ResponseWriter, r *http.Request) {
+func (h *handler) imageProxy(w http.ResponseWriter, r *http.Request) {
 	// If we receive a "If-None-Match" header, we assume the image is already stored in browser cache.
 	if r.Header.Get("If-None-Match") != "" {
 		w.WriteHeader(http.StatusNotModified)
diff --git a/ui/search_entries.go b/ui/search_entries.go
index 1304ef20..be97da0d 100644
--- a/ui/search_entries.go
+++ b/ui/search_entries.go
@@ -15,9 +15,8 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ShowSearchEntries shows all entries for the given feed.
-func (c *Controller) ShowSearchEntries(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) showSearchEntriesPage(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -25,7 +24,7 @@ func (c *Controller) ShowSearchEntries(w http.ResponseWriter, r *http.Request) {
 
 	searchQuery := request.QueryStringParam(r, "q", "")
 	offset := request.QueryIntParam(r, "offset", 0)
-	builder := c.store.NewEntryQueryBuilder(user.ID)
+	builder := h.store.NewEntryQueryBuilder(user.ID)
 	builder.WithSearchQuery(searchQuery)
 	builder.WithoutStatus(model.EntryStatusRemoved)
 	builder.WithOrder(model.DefaultSortingOrder)
@@ -45,9 +44,9 @@ func (c *Controller) ShowSearchEntries(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
-	pagination := c.getPagination(route.Path(c.router, "searchEntries"), count, offset)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
+	pagination := getPagination(route.Path(h.router, "searchEntries"), count, offset)
 	pagination.SearchQuery = searchQuery
 
 	view.Set("searchQuery", searchQuery)
@@ -56,9 +55,9 @@ func (c *Controller) ShowSearchEntries(w http.ResponseWriter, r *http.Request) {
 	view.Set("pagination", pagination)
 	view.Set("menu", "search")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
-	view.Set("hasSaveEntry", c.store.HasSaveEntry(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
+	view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
 
 	html.OK(w, r, view.Render("search_entries"))
 }
diff --git a/ui/session_list.go b/ui/session_list.go
index 76e693a1..28da2022 100644
--- a/ui/session_list.go
+++ b/ui/session_list.go
@@ -13,18 +13,17 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ShowSessions shows the list of active user sessions.
-func (c *Controller) ShowSessions(w http.ResponseWriter, r *http.Request) {
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+func (h *handler) showSessionsPage(w http.ResponseWriter, r *http.Request) {
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 
-	user, err := c.store.UserByID(request.UserID(r))
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	sessions, err := c.store.UserSessions(user.ID)
+	sessions, err := h.store.UserSessions(user.ID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -36,8 +35,8 @@ func (c *Controller) ShowSessions(w http.ResponseWriter, r *http.Request) {
 	view.Set("sessions", sessions)
 	view.Set("menu", "settings")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 
 	html.OK(w, r, view.Render("sessions"))
 }
diff --git a/ui/session_remove.go b/ui/session_remove.go
index cc626126..e84a7a2a 100644
--- a/ui/session_remove.go
+++ b/ui/session_remove.go
@@ -13,13 +13,12 @@ import (
 	"miniflux.app/logger"
 )
 
-// RemoveSession remove a user session.
-func (c *Controller) RemoveSession(w http.ResponseWriter, r *http.Request) {
+func (h *handler) removeSession(w http.ResponseWriter, r *http.Request) {
 	sessionID := request.RouteInt64Param(r, "sessionID")
-	err := c.store.RemoveUserSessionByID(request.UserID(r), sessionID)
+	err := h.store.RemoveUserSessionByID(request.UserID(r), sessionID)
 	if err != nil {
 		logger.Error("[Controller:RemoveSession] %v", err)
 	}
 
-	html.Redirect(w, r, route.Path(c.router, "sessions"))
+	html.Redirect(w, r, route.Path(h.router, "sessions"))
 }
diff --git a/ui/settings_show.go b/ui/settings_show.go
index 4f4756dc..39d85cac 100644
--- a/ui/settings_show.go
+++ b/ui/settings_show.go
@@ -16,12 +16,11 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ShowSettings shows the settings page.
-func (c *Controller) ShowSettings(w http.ResponseWriter, r *http.Request) {
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 
-	user, err := c.store.UserByID(request.UserID(r))
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -35,7 +34,7 @@ func (c *Controller) ShowSettings(w http.ResponseWriter, r *http.Request) {
 		EntryDirection: user.EntryDirection,
 	}
 
-	timezones, err := c.store.Timezones()
+	timezones, err := h.store.Timezones()
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -47,8 +46,8 @@ func (c *Controller) ShowSettings(w http.ResponseWriter, r *http.Request) {
 	view.Set("timezones", timezones)
 	view.Set("menu", "settings")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 
 	html.OK(w, r, view.Render("settings"))
 }
diff --git a/ui/settings_update.go b/ui/settings_update.go
index 466bb782..3497dd90 100644
--- a/ui/settings_update.go
+++ b/ui/settings_update.go
@@ -18,18 +18,17 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// UpdateSettings update the settings.
-func (c *Controller) UpdateSettings(w http.ResponseWriter, r *http.Request) {
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) {
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 
-	user, err := c.store.UserByID(request.UserID(r))
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	timezones, err := c.store.Timezones()
+	timezones, err := h.store.Timezones()
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -43,8 +42,8 @@ func (c *Controller) UpdateSettings(w http.ResponseWriter, r *http.Request) {
 	view.Set("timezones", timezones)
 	view.Set("menu", "settings")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 
 	if err := settingsForm.Validate(); err != nil {
 		view.Set("errorMessage", err.Error())
@@ -52,13 +51,13 @@ func (c *Controller) UpdateSettings(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if c.store.AnotherUserExists(user.ID, settingsForm.Username) {
+	if h.store.AnotherUserExists(user.ID, settingsForm.Username) {
 		view.Set("errorMessage", "error.user_already_exists")
 		html.OK(w, r, view.Render("settings"))
 		return
 	}
 
-	err = c.store.UpdateUser(settingsForm.Merge(user))
+	err = h.store.UpdateUser(settingsForm.Merge(user))
 	if err != nil {
 		logger.Error("[Controller:UpdateSettings] %v", err)
 		view.Set("errorMessage", "error.unable_to_update_user")
@@ -69,5 +68,5 @@ func (c *Controller) UpdateSettings(w http.ResponseWriter, r *http.Request) {
 	sess.SetLanguage(user.Language)
 	sess.SetTheme(user.Theme)
 	sess.NewFlashMessage(locale.NewPrinter(request.UserLanguage(r)).Printf("alert.prefs_saved"))
-	html.Redirect(w, r, route.Path(c.router, "settings"))
+	html.Redirect(w, r, route.Path(h.router, "settings"))
 }
diff --git a/ui/static_app_icon.go b/ui/static_app_icon.go
index 9e2a448d..ea0290b4 100644
--- a/ui/static_app_icon.go
+++ b/ui/static_app_icon.go
@@ -15,8 +15,7 @@ import (
 	"miniflux.app/ui/static"
 )
 
-// AppIcon shows application icons.
-func (c *Controller) AppIcon(w http.ResponseWriter, r *http.Request) {
+func (h *handler) showAppIcon(w http.ResponseWriter, r *http.Request) {
 	filename := request.RouteStringParam(r, "filename")
 	etag, found := static.BinariesChecksums[filename]
 	if !found {
diff --git a/ui/static_favicon.go b/ui/static_favicon.go
index 1266a199..80060a5e 100644
--- a/ui/static_favicon.go
+++ b/ui/static_favicon.go
@@ -14,8 +14,7 @@ import (
 	"miniflux.app/ui/static"
 )
 
-// Favicon shows the application favicon.
-func (c *Controller) Favicon(w http.ResponseWriter, r *http.Request) {
+func (h *handler) showFavicon(w http.ResponseWriter, r *http.Request) {
 	etag, found := static.BinariesChecksums["favicon.ico"]
 	if !found {
 		html.NotFound(w, r)
diff --git a/ui/static_javascript.go b/ui/static_javascript.go
index ff7fd16b..de6b342e 100644
--- a/ui/static_javascript.go
+++ b/ui/static_javascript.go
@@ -14,8 +14,7 @@ import (
 	"miniflux.app/ui/static"
 )
 
-// Javascript renders application client side code.
-func (c *Controller) Javascript(w http.ResponseWriter, r *http.Request) {
+func (h *handler) showJavascript(w http.ResponseWriter, r *http.Request) {
 	filename := request.RouteStringParam(r, "name")
 	etag, found := static.JavascriptsChecksums[filename]
 	if !found {
diff --git a/ui/static_manifest.go b/ui/static_manifest.go
index 51369a2e..50e7e988 100644
--- a/ui/static_manifest.go
+++ b/ui/static_manifest.go
@@ -13,8 +13,7 @@ import (
 	"miniflux.app/model"
 )
 
-// WebManifest renders web manifest file.
-func (c *Controller) WebManifest(w http.ResponseWriter, r *http.Request) {
+func (h *handler) showWebManifest(w http.ResponseWriter, r *http.Request) {
 	type webManifestIcon struct {
 		Source string `json:"src"`
 		Sizes  string `json:"sizes"`
@@ -38,13 +37,13 @@ func (c *Controller) WebManifest(w http.ResponseWriter, r *http.Request) {
 		ShortName:       "Miniflux",
 		Description:     "Minimalist Feed Reader",
 		Display:         "minimal-ui",
-		StartURL:        route.Path(c.router, "unread"),
+		StartURL:        route.Path(h.router, "unread"),
 		ThemeColor:      themeColor,
 		BackgroundColor: themeColor,
 		Icons: []webManifestIcon{
-			webManifestIcon{Source: route.Path(c.router, "appIcon", "filename", "icon-120.png"), Sizes: "120x120", Type: "image/png"},
-			webManifestIcon{Source: route.Path(c.router, "appIcon", "filename", "icon-192.png"), Sizes: "192x192", Type: "image/png"},
-			webManifestIcon{Source: route.Path(c.router, "appIcon", "filename", "icon-512.png"), Sizes: "512x512", Type: "image/png"},
+			webManifestIcon{Source: route.Path(h.router, "appIcon", "filename", "icon-120.png"), Sizes: "120x120", Type: "image/png"},
+			webManifestIcon{Source: route.Path(h.router, "appIcon", "filename", "icon-192.png"), Sizes: "192x192", Type: "image/png"},
+			webManifestIcon{Source: route.Path(h.router, "appIcon", "filename", "icon-512.png"), Sizes: "512x512", Type: "image/png"},
 		},
 	}
 
diff --git a/ui/static_stylesheet.go b/ui/static_stylesheet.go
index 9a21bc61..506caf44 100644
--- a/ui/static_stylesheet.go
+++ b/ui/static_stylesheet.go
@@ -14,8 +14,7 @@ import (
 	"miniflux.app/ui/static"
 )
 
-// Stylesheet renders the CSS.
-func (c *Controller) Stylesheet(w http.ResponseWriter, r *http.Request) {
+func (h *handler) showStylesheet(w http.ResponseWriter, r *http.Request) {
 	filename := request.RouteStringParam(r, "name")
 	etag, found := static.StylesheetsChecksums[filename]
 	if !found {
diff --git a/ui/subscription_add.go b/ui/subscription_add.go
index 3056eec9..db539c88 100644
--- a/ui/subscription_add.go
+++ b/ui/subscription_add.go
@@ -14,18 +14,17 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// AddSubscription shows the form to add a new feed.
-func (c *Controller) AddSubscription(w http.ResponseWriter, r *http.Request) {
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+func (h *handler) showAddSubscriptionPage(w http.ResponseWriter, r *http.Request) {
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 
-	user, err := c.store.UserByID(request.UserID(r))
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	categories, err := c.store.Categories(user.ID)
+	categories, err := h.store.Categories(user.ID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -34,8 +33,8 @@ func (c *Controller) AddSubscription(w http.ResponseWriter, r *http.Request) {
 	view.Set("categories", categories)
 	view.Set("menu", "feeds")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 	view.Set("defaultUserAgent", client.DefaultUserAgent)
 
 	html.OK(w, r, view.Render("add_subscription"))
diff --git a/ui/subscription_bookmarklet.go b/ui/subscription_bookmarklet.go
index 5c0e08c0..060aa486 100644
--- a/ui/subscription_bookmarklet.go
+++ b/ui/subscription_bookmarklet.go
@@ -15,18 +15,17 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// Bookmarklet prefill the form to add a subscription from the URL provided by the bookmarklet.
-func (c *Controller) Bookmarklet(w http.ResponseWriter, r *http.Request) {
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+func (h *handler) bookmarklet(w http.ResponseWriter, r *http.Request) {
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 
-	user, err := c.store.UserByID(request.UserID(r))
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	categories, err := c.store.Categories(user.ID)
+	categories, err := h.store.Categories(user.ID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -38,8 +37,8 @@ func (c *Controller) Bookmarklet(w http.ResponseWriter, r *http.Request) {
 	view.Set("categories", categories)
 	view.Set("menu", "feeds")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 	view.Set("defaultUserAgent", client.DefaultUserAgent)
 
 	html.OK(w, r, view.Render("add_subscription"))
diff --git a/ui/subscription_choose.go b/ui/subscription_choose.go
index 4f701c89..b554b037 100644
--- a/ui/subscription_choose.go
+++ b/ui/subscription_choose.go
@@ -16,18 +16,17 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ChooseSubscription shows a page to choose a subscription.
-func (c *Controller) ChooseSubscription(w http.ResponseWriter, r *http.Request) {
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+func (h *handler) showChooseSubscriptionPage(w http.ResponseWriter, r *http.Request) {
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 
-	user, err := c.store.UserByID(request.UserID(r))
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	categories, err := c.store.Categories(user.ID)
+	categories, err := h.store.Categories(user.ID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -36,8 +35,8 @@ func (c *Controller) ChooseSubscription(w http.ResponseWriter, r *http.Request)
 	view.Set("categories", categories)
 	view.Set("menu", "feeds")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 	view.Set("defaultUserAgent", client.DefaultUserAgent)
 
 	subscriptionForm := form.NewSubscriptionForm(r)
@@ -48,7 +47,7 @@ func (c *Controller) ChooseSubscription(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	feed, err := c.feedHandler.CreateFeed(
+	feed, err := h.feedHandler.CreateFeed(
 		user.ID,
 		subscriptionForm.CategoryID,
 		subscriptionForm.URL,
@@ -64,5 +63,5 @@ func (c *Controller) ChooseSubscription(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	html.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feed.ID))
+	html.Redirect(w, r, route.Path(h.router, "feedEntries", "feedID", feed.ID))
 }
diff --git a/ui/subscription_submit.go b/ui/subscription_submit.go
index bdf6a591..8d3c7ed1 100644
--- a/ui/subscription_submit.go
+++ b/ui/subscription_submit.go
@@ -18,18 +18,17 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// SubmitSubscription try to find a feed from the URL provided by the user.
-func (c *Controller) SubmitSubscription(w http.ResponseWriter, r *http.Request) {
-	sess := session.New(c.store, request.SessionID(r))
-	v := view.New(c.tpl, r, sess)
+func (h *handler) submitSubscription(w http.ResponseWriter, r *http.Request) {
+	sess := session.New(h.store, request.SessionID(r))
+	v := view.New(h.tpl, r, sess)
 
-	user, err := c.store.UserByID(request.UserID(r))
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	categories, err := c.store.Categories(user.ID)
+	categories, err := h.store.Categories(user.ID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -38,8 +37,8 @@ func (c *Controller) SubmitSubscription(w http.ResponseWriter, r *http.Request)
 	v.Set("categories", categories)
 	v.Set("menu", "feeds")
 	v.Set("user", user)
-	v.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	v.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	v.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	v.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 	v.Set("defaultUserAgent", client.DefaultUserAgent)
 
 	subscriptionForm := form.NewSubscriptionForm(r)
@@ -73,7 +72,7 @@ func (c *Controller) SubmitSubscription(w http.ResponseWriter, r *http.Request)
 		v.Set("errorMessage", "error.subscription_not_found")
 		html.OK(w, r, v.Render("add_subscription"))
 	case n == 1:
-		feed, err := c.feedHandler.CreateFeed(
+		feed, err := h.feedHandler.CreateFeed(
 			user.ID,
 			subscriptionForm.CategoryID,
 			subscriptions[0].URL,
@@ -89,15 +88,15 @@ func (c *Controller) SubmitSubscription(w http.ResponseWriter, r *http.Request)
 			return
 		}
 
-		html.Redirect(w, r, route.Path(c.router, "feedEntries", "feedID", feed.ID))
+		html.Redirect(w, r, route.Path(h.router, "feedEntries", "feedID", feed.ID))
 	case n > 1:
-		v := view.New(c.tpl, r, sess)
+		v := view.New(h.tpl, r, sess)
 		v.Set("subscriptions", subscriptions)
 		v.Set("form", subscriptionForm)
 		v.Set("menu", "feeds")
 		v.Set("user", user)
-		v.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-		v.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+		v.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+		v.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 
 		html.OK(w, r, v.Render("choose_subscription"))
 	}
diff --git a/ui/ui.go b/ui/ui.go
new file mode 100644
index 00000000..35776368
--- /dev/null
+++ b/ui/ui.go
@@ -0,0 +1,133 @@
+// 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 ui // import "miniflux.app/ui"
+
+import (
+	"net/http"
+
+	"miniflux.app/config"
+	"miniflux.app/reader/feed"
+	"miniflux.app/scheduler"
+	"miniflux.app/storage"
+	"miniflux.app/template"
+
+	"github.com/gorilla/mux"
+)
+
+// Serve declares all routes for the user interface.
+func Serve(router *mux.Router, cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler) {
+	middleware := newMiddleware(router, cfg, store)
+	handler := &handler{router, cfg, store, template.NewEngine(cfg, router), pool, feedHandler}
+
+	uiRouter := router.NewRoute().Subrouter()
+	uiRouter.Use(middleware.handleAppSession)
+	uiRouter.Use(middleware.handleUserSession)
+
+	// Static assets.
+	uiRouter.HandleFunc("/stylesheets/{name}.css", handler.showStylesheet).Name("stylesheet").Methods("GET")
+	uiRouter.HandleFunc("/{name}.js", handler.showJavascript).Name("javascript").Methods("GET")
+	uiRouter.HandleFunc("/favicon.ico", handler.showFavicon).Name("favicon").Methods("GET")
+	uiRouter.HandleFunc("/icon/{filename}", handler.showAppIcon).Name("appIcon").Methods("GET")
+	uiRouter.HandleFunc("/manifest.json", handler.showWebManifest).Name("webManifest").Methods("GET")
+
+	// New subscription pages.
+	uiRouter.HandleFunc("/subscribe", handler.showAddSubscriptionPage).Name("addSubscription").Methods("GET")
+	uiRouter.HandleFunc("/subscribe", handler.submitSubscription).Name("submitSubscription").Methods("POST")
+	uiRouter.HandleFunc("/subscriptions", handler.showChooseSubscriptionPage).Name("chooseSubscription").Methods("POST")
+	uiRouter.HandleFunc("/bookmarklet", handler.bookmarklet).Name("bookmarklet").Methods("GET")
+
+	// Unread page.
+	uiRouter.HandleFunc("/mark-all-as-read", handler.markAllAsRead).Name("markAllAsRead").Methods("GET")
+	uiRouter.HandleFunc("/unread", handler.showUnreadPage).Name("unread").Methods("GET")
+	uiRouter.HandleFunc("/unread/entry/{entryID}", handler.showUnreadEntryPage).Name("unreadEntry").Methods("GET")
+
+	// History pages.
+	uiRouter.HandleFunc("/history", handler.showHistoryPage).Name("history").Methods("GET")
+	uiRouter.HandleFunc("/history/entry/{entryID}", handler.showReadEntryPage).Name("readEntry").Methods("GET")
+	uiRouter.HandleFunc("/history/flush", handler.flushHistory).Name("flushHistory").Methods("GET")
+
+	// Bookmark pages.
+	uiRouter.HandleFunc("/starred", handler.showStarredPage).Name("starred").Methods("GET")
+	uiRouter.HandleFunc("/starred/entry/{entryID}", handler.showStarredEntryPage).Name("starredEntry").Methods("GET")
+
+	// Search pages.
+	uiRouter.HandleFunc("/search", handler.showSearchEntriesPage).Name("searchEntries").Methods("GET")
+	uiRouter.HandleFunc("/search/entry/{entryID}", handler.showSearchEntryPage).Name("searchEntry").Methods("GET")
+
+	// Feed listing pages.
+	uiRouter.HandleFunc("/feeds", handler.showFeedsPage).Name("feeds").Methods("GET")
+	uiRouter.HandleFunc("/feeds/refresh", handler.refreshAllFeeds).Name("refreshAllFeeds").Methods("GET")
+
+	// Individual feed pages.
+	uiRouter.HandleFunc("/feed/{feedID}/refresh", handler.refreshFeed).Name("refreshFeed").Methods("GET")
+	uiRouter.HandleFunc("/feed/{feedID}/edit", handler.showEditFeedPage).Name("editFeed").Methods("GET")
+	uiRouter.HandleFunc("/feed/{feedID}/remove", handler.removeFeed).Name("removeFeed").Methods("POST")
+	uiRouter.HandleFunc("/feed/{feedID}/update", handler.updateFeed).Name("updateFeed").Methods("POST")
+	uiRouter.HandleFunc("/feed/{feedID}/entries", handler.showFeedEntriesPage).Name("feedEntries").Methods("GET")
+	uiRouter.HandleFunc("/feed/{feedID}/entry/{entryID}", handler.showFeedEntryPage).Name("feedEntry").Methods("GET")
+	uiRouter.HandleFunc("/feed/icon/{iconID}", handler.showIcon).Name("icon").Methods("GET")
+
+	// Category pages.
+	uiRouter.HandleFunc("/category/{categoryID}/entry/{entryID}", handler.showCategoryEntryPage).Name("categoryEntry").Methods("GET")
+	uiRouter.HandleFunc("/categories", handler.showCategoryListPage).Name("categories").Methods("GET")
+	uiRouter.HandleFunc("/category/create", handler.showCreateCategoryPage).Name("createCategory").Methods("GET")
+	uiRouter.HandleFunc("/category/save", handler.saveCategory).Name("saveCategory").Methods("POST")
+	uiRouter.HandleFunc("/category/{categoryID}/entries", handler.showCategoryEntriesPage).Name("categoryEntries").Methods("GET")
+	uiRouter.HandleFunc("/category/{categoryID}/edit", handler.showEditCategoryPage).Name("editCategory").Methods("GET")
+	uiRouter.HandleFunc("/category/{categoryID}/update", handler.updateCategory).Name("updateCategory").Methods("POST")
+	uiRouter.HandleFunc("/category/{categoryID}/remove", handler.removeCategory).Name("removeCategory").Methods("POST")
+
+	// Entry pages.
+	uiRouter.HandleFunc("/entry/status", handler.updateEntriesStatus).Name("updateEntriesStatus").Methods("POST")
+	uiRouter.HandleFunc("/entry/save/{entryID}", handler.saveEntry).Name("saveEntry").Methods("POST")
+	uiRouter.HandleFunc("/entry/download/{entryID}", handler.fetchContent).Name("fetchContent").Methods("POST")
+	uiRouter.HandleFunc("/proxy/{encodedURL}", handler.imageProxy).Name("proxy").Methods("GET")
+	uiRouter.HandleFunc("/entry/bookmark/{entryID}", handler.toggleBookmark).Name("toggleBookmark").Methods("POST")
+
+	// User pages.
+	uiRouter.HandleFunc("/users", handler.showUsersPage).Name("users").Methods("GET")
+	uiRouter.HandleFunc("/user/create", handler.showCreateUserPage).Name("createUser").Methods("GET")
+	uiRouter.HandleFunc("/user/save", handler.saveUser).Name("saveUser").Methods("POST")
+	uiRouter.HandleFunc("/users/{userID}/edit", handler.showEditUserPage).Name("editUser").Methods("GET")
+	uiRouter.HandleFunc("/users/{userID}/update", handler.updateUser).Name("updateUser").Methods("POST")
+	uiRouter.HandleFunc("/users/{userID}/remove", handler.removeUser).Name("removeUser").Methods("POST")
+
+	// Settings pages.
+	uiRouter.HandleFunc("/settings", handler.showSettingsPage).Name("settings").Methods("GET")
+	uiRouter.HandleFunc("/settings", handler.updateSettings).Name("updateSettings").Methods("POST")
+	uiRouter.HandleFunc("/integrations", handler.showIntegrationPage).Name("integrations").Methods("GET")
+	uiRouter.HandleFunc("/integration", handler.updateIntegration).Name("updateIntegration").Methods("POST")
+	uiRouter.HandleFunc("/integration/pocket/authorize", handler.pocketAuthorize).Name("pocketAuthorize").Methods("GET")
+	uiRouter.HandleFunc("/integration/pocket/callback", handler.pocketCallback).Name("pocketCallback").Methods("GET")
+	uiRouter.HandleFunc("/about", handler.showAboutPage).Name("about").Methods("GET")
+
+	// Session pages.
+	uiRouter.HandleFunc("/sessions", handler.showSessionsPage).Name("sessions").Methods("GET")
+	uiRouter.HandleFunc("/sessions/{sessionID}/remove", handler.removeSession).Name("removeSession").Methods("POST")
+
+	// OPML pages.
+	uiRouter.HandleFunc("/export", handler.exportFeeds).Name("export").Methods("GET")
+	uiRouter.HandleFunc("/import", handler.showImportPage).Name("import").Methods("GET")
+	uiRouter.HandleFunc("/upload", handler.uploadOPML).Name("uploadOPML").Methods("POST")
+
+	// OAuth2 flow.
+	uiRouter.HandleFunc("/oauth2/{provider}/unlink", handler.oauth2Unlink).Name("oauth2Unlink").Methods("GET")
+	uiRouter.HandleFunc("/oauth2/{provider}/redirect", handler.oauth2Redirect).Name("oauth2Redirect").Methods("GET")
+	uiRouter.HandleFunc("/oauth2/{provider}/callback", handler.oauth2Callback).Name("oauth2Callback").Methods("GET")
+
+	// Authentication pages.
+	uiRouter.HandleFunc("/login", handler.checkLogin).Name("checkLogin").Methods("POST")
+	uiRouter.HandleFunc("/logout", handler.logout).Name("logout").Methods("GET")
+	uiRouter.HandleFunc("/", handler.showLoginPage).Name("login").Methods("GET")
+
+	router.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte("OK"))
+	}).Name("healthcheck")
+
+	router.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "text/plain")
+		w.Write([]byte("User-agent: *\nDisallow: /"))
+	}).Name("robots")
+}
diff --git a/ui/unread_entries.go b/ui/unread_entries.go
index cb024282..c49fc30b 100644
--- a/ui/unread_entries.go
+++ b/ui/unread_entries.go
@@ -15,19 +15,18 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ShowUnreadPage render the page with all unread entries.
-func (c *Controller) ShowUnreadPage(w http.ResponseWriter, r *http.Request) {
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+func (h *handler) showUnreadPage(w http.ResponseWriter, r *http.Request) {
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 
-	user, err := c.store.UserByID(request.UserID(r))
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
 	offset := request.QueryIntParam(r, "offset", 0)
-	builder := c.store.NewEntryQueryBuilder(user.ID)
+	builder := h.store.NewEntryQueryBuilder(user.ID)
 	builder.WithStatus(model.EntryStatusUnread)
 	countUnread, err := builder.CountEntries()
 	if err != nil {
@@ -39,7 +38,7 @@ func (c *Controller) ShowUnreadPage(w http.ResponseWriter, r *http.Request) {
 		offset = 0
 	}
 
-	builder = c.store.NewEntryQueryBuilder(user.ID)
+	builder = h.store.NewEntryQueryBuilder(user.ID)
 	builder.WithStatus(model.EntryStatusUnread)
 	builder.WithOrder(model.DefaultSortingOrder)
 	builder.WithDirection(user.EntryDirection)
@@ -52,12 +51,12 @@ func (c *Controller) ShowUnreadPage(w http.ResponseWriter, r *http.Request) {
 	}
 
 	view.Set("entries", entries)
-	view.Set("pagination", c.getPagination(route.Path(c.router, "unread"), countUnread, offset))
+	view.Set("pagination", getPagination(route.Path(h.router, "unread"), countUnread, offset))
 	view.Set("menu", "unread")
 	view.Set("user", user)
 	view.Set("countUnread", countUnread)
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
-	view.Set("hasSaveEntry", c.store.HasSaveEntry(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
+	view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
 
 	html.OK(w, r, view.Render("unread_entries"))
 }
diff --git a/ui/unread_mark_all_read.go b/ui/unread_mark_all_read.go
index 724da67e..c7b9fa0a 100644
--- a/ui/unread_mark_all_read.go
+++ b/ui/unread_mark_all_read.go
@@ -13,11 +13,10 @@ import (
 	"miniflux.app/logger"
 )
 
-// MarkAllAsRead marks all unread entries as read.
-func (c *Controller) MarkAllAsRead(w http.ResponseWriter, r *http.Request) {
-	if err := c.store.MarkAllAsRead(request.UserID(r)); err != nil {
+func (h *handler) markAllAsRead(w http.ResponseWriter, r *http.Request) {
+	if err := h.store.MarkAllAsRead(request.UserID(r)); err != nil {
 		logger.Error("[MarkAllAsRead] %v", err)
 	}
 
-	html.Redirect(w, r, route.Path(c.router, "unread"))
+	html.Redirect(w, r, route.Path(h.router, "unread"))
 }
diff --git a/ui/user_create.go b/ui/user_create.go
index a94d286a..c3269b22 100644
--- a/ui/user_create.go
+++ b/ui/user_create.go
@@ -14,12 +14,11 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// CreateUser shows the user creation form.
-func (c *Controller) CreateUser(w http.ResponseWriter, r *http.Request) {
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+func (h *handler) showCreateUserPage(w http.ResponseWriter, r *http.Request) {
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 
-	user, err := c.store.UserByID(request.UserID(r))
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -33,8 +32,8 @@ func (c *Controller) CreateUser(w http.ResponseWriter, r *http.Request) {
 	view.Set("form", &form.UserForm{})
 	view.Set("menu", "settings")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 
 	html.OK(w, r, view.Render("create_user"))
 }
diff --git a/ui/user_edit.go b/ui/user_edit.go
index 2348a7dd..3d0289ce 100644
--- a/ui/user_edit.go
+++ b/ui/user_edit.go
@@ -15,11 +15,11 @@ import (
 )
 
 // EditUser shows the form to edit a user.
-func (c *Controller) EditUser(w http.ResponseWriter, r *http.Request) {
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+func (h *handler) showEditUserPage(w http.ResponseWriter, r *http.Request) {
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 
-	user, err := c.store.UserByID(request.UserID(r))
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -31,7 +31,7 @@ func (c *Controller) EditUser(w http.ResponseWriter, r *http.Request) {
 	}
 
 	userID := request.RouteInt64Param(r, "userID")
-	selectedUser, err := c.store.UserByID(userID)
+	selectedUser, err := h.store.UserByID(userID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -51,8 +51,8 @@ func (c *Controller) EditUser(w http.ResponseWriter, r *http.Request) {
 	view.Set("selected_user", selectedUser)
 	view.Set("menu", "settings")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 
 	html.OK(w, r, view.Render("edit_user"))
 }
diff --git a/ui/user_list.go b/ui/user_list.go
index 3c9e3edd..37850c71 100644
--- a/ui/user_list.go
+++ b/ui/user_list.go
@@ -13,12 +13,11 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// ShowUsers renders the list of users.
-func (c *Controller) ShowUsers(w http.ResponseWriter, r *http.Request) {
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+func (h *handler) showUsersPage(w http.ResponseWriter, r *http.Request) {
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 
-	user, err := c.store.UserByID(request.UserID(r))
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -29,7 +28,7 @@ func (c *Controller) ShowUsers(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	users, err := c.store.Users()
+	users, err := h.store.Users()
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -40,8 +39,8 @@ func (c *Controller) ShowUsers(w http.ResponseWriter, r *http.Request) {
 	view.Set("users", users)
 	view.Set("menu", "settings")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 
 	html.OK(w, r, view.Render("users"))
 }
diff --git a/ui/user_remove.go b/ui/user_remove.go
index beda7be9..6564ca32 100644
--- a/ui/user_remove.go
+++ b/ui/user_remove.go
@@ -12,9 +12,8 @@ import (
 	"miniflux.app/http/route"
 )
 
-// RemoveUser deletes a user from the database.
-func (c *Controller) RemoveUser(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) removeUser(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -26,7 +25,7 @@ func (c *Controller) RemoveUser(w http.ResponseWriter, r *http.Request) {
 	}
 
 	userID := request.RouteInt64Param(r, "userID")
-	selectedUser, err := c.store.UserByID(userID)
+	selectedUser, err := h.store.UserByID(userID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -37,10 +36,10 @@ func (c *Controller) RemoveUser(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if err := c.store.RemoveUser(selectedUser.ID); err != nil {
+	if err := h.store.RemoveUser(selectedUser.ID); err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	html.Redirect(w, r, route.Path(c.router, "users"))
+	html.Redirect(w, r, route.Path(h.router, "users"))
 }
diff --git a/ui/user_save.go b/ui/user_save.go
index a4be3465..f13ac0aa 100644
--- a/ui/user_save.go
+++ b/ui/user_save.go
@@ -16,9 +16,8 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// SaveUser validate and save the new user into the database.
-func (c *Controller) SaveUser(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) saveUser(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -31,12 +30,12 @@ func (c *Controller) SaveUser(w http.ResponseWriter, r *http.Request) {
 
 	userForm := form.NewUserForm(r)
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("menu", "settings")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 	view.Set("form", userForm)
 
 	if err := userForm.ValidateCreation(); err != nil {
@@ -45,19 +44,19 @@ func (c *Controller) SaveUser(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if c.store.UserExists(userForm.Username) {
+	if h.store.UserExists(userForm.Username) {
 		view.Set("errorMessage", "error.user_already_exists")
 		html.OK(w, r, view.Render("create_user"))
 		return
 	}
 
 	newUser := userForm.ToUser()
-	if err := c.store.CreateUser(newUser); err != nil {
+	if err := h.store.CreateUser(newUser); err != nil {
 		logger.Error("[Controller:SaveUser] %v", err)
 		view.Set("errorMessage", "error.unable_to_create_user")
 		html.OK(w, r, view.Render("create_user"))
 		return
 	}
 
-	html.Redirect(w, r, route.Path(c.router, "users"))
+	html.Redirect(w, r, route.Path(h.router, "users"))
 }
diff --git a/ui/user_update.go b/ui/user_update.go
index 34567e03..85f15010 100644
--- a/ui/user_update.go
+++ b/ui/user_update.go
@@ -16,9 +16,8 @@ import (
 	"miniflux.app/ui/view"
 )
 
-// UpdateUser validate and update a user.
-func (c *Controller) UpdateUser(w http.ResponseWriter, r *http.Request) {
-	user, err := c.store.UserByID(request.UserID(r))
+func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -30,7 +29,7 @@ func (c *Controller) UpdateUser(w http.ResponseWriter, r *http.Request) {
 	}
 
 	userID := request.RouteInt64Param(r, "userID")
-	selectedUser, err := c.store.UserByID(userID)
+	selectedUser, err := h.store.UserByID(userID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
@@ -43,12 +42,12 @@ func (c *Controller) UpdateUser(w http.ResponseWriter, r *http.Request) {
 
 	userForm := form.NewUserForm(r)
 
-	sess := session.New(c.store, request.SessionID(r))
-	view := view.New(c.tpl, r, sess)
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
 	view.Set("menu", "settings")
 	view.Set("user", user)
-	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
-	view.Set("countErrorFeeds", c.store.CountErrorFeeds(user.ID))
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
 	view.Set("selected_user", selectedUser)
 	view.Set("form", userForm)
 
@@ -58,19 +57,19 @@ func (c *Controller) UpdateUser(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if c.store.AnotherUserExists(selectedUser.ID, userForm.Username) {
+	if h.store.AnotherUserExists(selectedUser.ID, userForm.Username) {
 		view.Set("errorMessage", "error.user_already_exists")
 		html.OK(w, r, view.Render("edit_user"))
 		return
 	}
 
 	userForm.Merge(selectedUser)
-	if err := c.store.UpdateUser(selectedUser); err != nil {
+	if err := h.store.UpdateUser(selectedUser); err != nil {
 		logger.Error("[Controller:UpdateUser] %v", err)
 		view.Set("errorMessage", "error.unable_to_update_user")
 		html.OK(w, r, view.Render("edit_user"))
 		return
 	}
 
-	html.Redirect(w, r, route.Path(c.router, "users"))
+	html.Redirect(w, r, route.Path(h.router, "users"))
 }