Repository: Ptt-official-app/Ptt-backend
Pull request #23 (2021-01-31 07:04:00)

Feat/clean architecture
## 👏 解決掉的 issue / Resolved Issues
- https://github.com/Ptt-official-app/Ptt-backend/issues/16
## 📝 相關的 issue / Related Issues
無

## ⛏ 變更內容 / Details of Changes
- 根據 @mkfsn 大大的 https://github.com/mkfsn/Ptt-backend/tree/feat/clean-architecture 改成一個 usecase 以及一個 repository，_並把 *bbs.DB 取代為 db.DB 這個 interface_ <- 已把 db interface 移除
- 把 config file 放到 conf 資料夾內
- 增加 usecase, repository, db 這三個的 mock，放在 mock 資料夾內
- 做 unit test 時基本上就是
  寫 delivery 測試時（測試route相關功能），mock usecase 然後 DI 來去除測試與 usecase 相依
  寫 usecase 測試時（測試業務邏輯），mock repository 然後 DI 來去該測試與 repository 讀資料的相依
  _寫 repository 測試時（測試讀資料功能），mock db 然後 DI 來去除測試與 *bbs.DB 相依_ <- 已把 db interface 移除

- 因為 main.go 放到 cmd 內了，所以現在要去 cmd 裡面用 go build (build 出來的東西會叫做 cmd)，且因為現在 config file path 是寫死(目前執行位置的相對路徑)，必須回到 Ptt-backend 跟目錄下指令 cmd/cmd 執行

## 衍生 issue
- config file 在下指令的時候用參數傳進去會比較方便
- 滿多 function 目前都是用 interface 當參數或回傳值，建議全部成固定的 type

應該多少會有地方做得不好，請大家看看有什麼需要改的地方
謝謝～


Patch:
diff --git a/cmd/main.go b/cmd/main.go
new file mode 100644
index 0000000..e584e96
--- /dev/null
+++ b/cmd/main.go
@@ -0,0 +1,41 @@
+package main
+
+import (
+	"github.com/Ptt-official-app/Ptt-backend/internal/config"
+	"github.com/Ptt-official-app/Ptt-backend/internal/delivery/http"
+	"github.com/Ptt-official-app/Ptt-backend/internal/logging"
+	"github.com/Ptt-official-app/Ptt-backend/internal/repository"
+	"github.com/Ptt-official-app/Ptt-backend/internal/usecase"
+
+	"github.com/PichuChen/go-bbs"
+	_ "github.com/PichuChen/go-bbs/pttbbs"
+)
+
+func main() {
+	var logger = logging.NewLogger()
+
+	logger.Informationalf("server start")
+
+	globalConfig, err := config.NewDefaultConfig()
+	if err != nil {
+		logger.Errorf("failed to get config: %v", err)
+		return
+	}
+
+	db, err := bbs.Open("pttbbs", globalConfig.BBSHome)
+	if err != nil {
+		logger.Errorf("open bbs db error: %v", err)
+		return
+	}
+
+	repo, err := repository.NewRepository(db)
+	if err != nil {
+		logger.Errorf("failed to create user repository: %s\n", err)
+		return
+	}
+	usecase := usecase.NewUsecase(globalConfig, repo)
+	httpDelivery := http.NewHTTPDelivery(usecase)
+	if err := httpDelivery.Run(globalConfig.ListenPort); err != nil {
+		logger.Errorf("run http delivery error: %s\n", err)
+	}
+}
diff --git a/config_default.toml b/conf/config_default.toml
similarity index 100%
rename from config_default.toml
rename to conf/config_default.toml
diff --git a/config.go b/config.go
deleted file mode 100644
index a67de71..0000000
--- a/config.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package main
-
-import (
-	"github.com/pelletier/go-toml"
-	"time"
-)
-
-type GlobalConfig struct {
-	BBSHome               string
-	ListenPort            int16
-	AccessTokenPrivateKey string
-	AccessTokenPublicKey  string
-	AccessTokenExpiresAt  time.Duration
-}
-
-var globalConfig = GlobalConfig{}
-
-func loadDefaultConfig() {
-	logger.Debugf("load default config")
-	defaultConfig, err := toml.LoadFile("config_default.toml")
-	if err != nil {
-		logger.Errorf("load default config error: %v", err)
-		return
-	}
-	applyConfig(defaultConfig)
-
-	logger.Debugf("load user config")
-	config, err := toml.LoadFile("config.toml")
-	if err != nil {
-		logger.Warningf("load custom config error: %v", err)
-		return
-	}
-	applyConfig(config)
-
-}
-
-func applyConfig(config *toml.Tree) {
-	logger.Debugf("apply config")
-	var s string
-	var i int64
-	var ok bool
-
-	s, ok = config.Get("bbs.home").(string)
-	if ok {
-		logger.Debugf("read config bbs.home: %s", s)
-		globalConfig.BBSHome = s
-	}
-	i, ok = config.Get("networking.listen_port").(int64)
-	if ok {
-		logger.Debugf("read config networking.listen_port: %v", i)
-		globalConfig.ListenPort = int16(i)
-	}
-	s, ok = config.Get("security.access_token_private_key").(string)
-	if ok {
-		logger.Debugf("read config security.access_token_private_key")
-		globalConfig.AccessTokenPrivateKey = s
-	}
-	s, ok = config.Get("security.access_token_public_key").(string)
-	if ok {
-		logger.Debugf("read config security.access_token_public_key")
-		globalConfig.AccessTokenPublicKey = s
-	}
-
-	s, ok = config.Get("security.access_token_expires_at").(string)
-	if ok {
-		var err error
-		logger.Debugf("read config security.access_token_expires_at: %v", s)
-		globalConfig.AccessTokenExpiresAt, err = time.ParseDuration(s)
-		if err != nil {
-			logger.Warningf("parse duartion failed: %v", err)
-		}
-	}
-
-}
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..03bf061
--- /dev/null
+++ b/internal/config/config.go
@@ -0,0 +1,80 @@
+package config
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/Ptt-official-app/Ptt-backend/internal/logging"
+	"github.com/pelletier/go-toml"
+)
+
+type Config struct {
+	BBSHome               string
+	ListenPort            int16
+	AccessTokenPrivateKey string
+	AccessTokenPublicKey  string
+	AccessTokenExpiresAt  time.Duration
+}
+
+var (
+	logger = logging.NewLogger()
+)
+
+func NewDefaultConfig() (*Config, error) {
+	var config *Config
+	config = &Config{}
+	logger.Debugf("load default config")
+
+	defaultConfig, err := toml.LoadFile("./conf/config_default.toml")
+	if err != nil {
+		return nil, fmt.Errorf("failed to load default config: %w", err)
+	}
+	applyConfig(config, defaultConfig)
+
+	logger.Debugf("load user config")
+	userConfig, err := toml.LoadFile("config.toml")
+	if err != nil {
+		return config, nil
+	}
+	applyConfig(config, userConfig)
+
+	return config, nil
+}
+
+func applyConfig(config *Config, rawConfig *toml.Tree) {
+	logger.Debugf("apply rawConfig")
+	var s string
+	var i int64
+	var ok bool
+
+	s, ok = rawConfig.Get("bbs.home").(string)
+	if ok {
+		logger.Debugf("read rawConfig bbs.home: %s", s)
+		config.BBSHome = s
+	}
+	i, ok = rawConfig.Get("networking.listen_port").(int64)
+	if ok {
+		logger.Debugf("read rawConfig networking.listen_port: %v", i)
+		config.ListenPort = int16(i)
+	}
+	s, ok = rawConfig.Get("security.access_token_private_key").(string)
+	if ok {
+		logger.Debugf("read rawConfig security.access_token_private_key")
+		config.AccessTokenPrivateKey = s
+	}
+	s, ok = rawConfig.Get("security.access_token_public_key").(string)
+	if ok {
+		logger.Debugf("read rawConfig security.access_token_public_key")
+		config.AccessTokenPublicKey = s
+	}
+
+	s, ok = rawConfig.Get("security.access_token_expires_at").(string)
+	if ok {
+		var err error
+		logger.Debugf("read rawConfig security.access_token_expires_at: %v", s)
+		config.AccessTokenExpiresAt, err = time.ParseDuration(s)
+		if err != nil {
+			logger.Warningf("parse duration failed: %v", err)
+		}
+	}
+}
diff --git a/internal/delivery/http/http.go b/internal/delivery/http/http.go
new file mode 100644
index 0000000..b0e52ab
--- /dev/null
+++ b/internal/delivery/http/http.go
@@ -0,0 +1,30 @@
+package http
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/Ptt-official-app/Ptt-backend/internal/logging"
+	"github.com/Ptt-official-app/Ptt-backend/internal/usecase"
+)
+
+type httpDelivery struct {
+	logger  logging.Logger
+	usecase usecase.Usecase
+}
+
+func NewHTTPDelivery(usecase usecase.Usecase) *httpDelivery {
+	delivery := &httpDelivery{
+		logger:  logging.NewLogger(),
+		usecase: usecase,
+	}
+	return delivery
+}
+
+func (delivery *httpDelivery) Run(port int16) error {
+	mux := http.NewServeMux()
+	delivery.buildRoute(mux)
+
+	delivery.logger.Informationalf("listen port on %v", port)
+	return http.ListenAndServe(fmt.Sprintf(":%v", port), mux)
+}
diff --git a/internal/delivery/http/route.go b/internal/delivery/http/route.go
new file mode 100644
index 0000000..166effa
--- /dev/null
+++ b/internal/delivery/http/route.go
@@ -0,0 +1,179 @@
+package http
+
+import (
+	"net/http"
+	"strings"
+)
+
+func (delivery *httpDelivery) buildRoute(mux *http.ServeMux) {
+	mux.HandleFunc("/v1/token", delivery.routeToken)
+	mux.HandleFunc("/v1/boards", delivery.routeBoards)
+	mux.HandleFunc("/v1/boards/", delivery.routeBoards)
+	mux.HandleFunc("/v1/classes/", delivery.routeClasses)
+	mux.HandleFunc("/v1/users/", delivery.routeUsers)
+}
+
+func (delivery *httpDelivery) routeToken(w http.ResponseWriter, r *http.Request) {
+	// TODO: Check IP Flowspeed
+	switch r.Method {
+	case http.MethodPost:
+		delivery.postToken(w, r)
+	}
+}
+
+func (delivery *httpDelivery) routeClass(w http.ResponseWriter, r *http.Request) {
+	switch r.Method {
+	case http.MethodGet:
+		delivery.getClass(w, r)
+	}
+}
+
+// routeBoards is the handler for `/v1/boards`
+func (delivery *httpDelivery) routeBoards(w http.ResponseWriter, r *http.Request) {
+	delivery.logger.Debugf("routeBoards: %v", r)
+	// TODO: Check IP Flowspeed
+	switch r.Method {
+	case http.MethodGet:
+		delivery.getBoards(w, r)
+	}
+}
+
+// routeClasses is the handler for `/v1/classes`
+func (delivery *httpDelivery) routeClasses(w http.ResponseWriter, r *http.Request) {
+	// TODO: Check IP Flowspeed
+	switch r.Method {
+	case http.MethodGet:
+		delivery.getClasses(w, r)
+	}
+}
+
+func (delivery *httpDelivery) routeUsers(w http.ResponseWriter, r *http.Request) {
+	// TODO: Check IP Flowspeed
+	switch r.Method {
+	case http.MethodGet:
+		delivery.getUsers(w, r)
+	}
+}
+
+// getBoards is the handler for `/v1/boards` with GET method
+func (delivery *httpDelivery) getBoards(w http.ResponseWriter, r *http.Request) {
+	delivery.logger.Debugf("getBoards: %v", r)
+	boardId, item, filename, err := delivery.parseBoardPath(r.URL.Path)
+	if boardId == "" {
+		delivery.getBoardList(w, r)
+		return
+	}
+	// get single board
+	if item == "information" {
+		delivery.getBoardInformation(w, r, boardId)
+		return
+	} else if item == "articles" {
+		if filename == "" {
+			delivery.getBoardArticles(w, r, boardId)
+		} else {
+			delivery.getBoardArticlesFile(w, r, boardId, filename)
+		}
+		return
+	} else if item == "treasures" {
+		delivery.getBoardTreasures(w, r, boardId)
+		return
+	}
+
+	// 404
+	w.WriteHeader(http.StatusNotFound)
+
+	delivery.logger.Noticef("board id: %v not exist but be queried, info: %v err: %v", boardId, item, err)
+}
+
+// parseBoardPath covert url path from /v1/boards/SYSOP/article to
+// {SYSOP, article) or /v1/boards to {,}
+func (delivery *httpDelivery) parseBoardPath(path string) (boardId string, item string, filename string, err error) {
+	pathSegment := strings.Split(path, "/")
+
+	if len(pathSegment) >= 6 {
+		// /{{version}}/boards/{{class_id}}/{{item}}/{{filename}}
+		boardId = pathSegment[3]
+		item = pathSegment[4]
+		filename = pathSegment[5]
+		return
+	} else if len(pathSegment) == 5 {
+		// /{{version}}/boards/{{class_id}}/{{item}}
+		boardId = pathSegment[3]
+		item = pathSegment[4]
+		return
+	} else if len(pathSegment) == 4 {
+		// /{{version}}/boards/{{class_id}}
+		boardId = pathSegment[3]
+		return
+	} else if len(pathSegment) == 3 {
+		// /{{version}}/boards
+		// Should not be reach...
+		return
+	}
+	delivery.logger.Warningf("parseBoardPath got malform path: %v", path)
+	return
+}
+
+// parseBoardTreasurePath parse covert url path from /v1/boards/SYSOP/article to
+// {SYSOP, article) or /v1/boards to {,}
+func (delivery *httpDelivery) parseBoardTreasurePath(path string) (boardId string, treasuresId []string, filename string, err error) {
+	pathSegment := strings.Split(path, "/")
+
+	if len(pathSegment) == 6 {
+		// /{{version}}/boards/{{board_id}}/treasures/articles
+		boardId = pathSegment[3]
+		treasuresId = []string{}
+		filename = ""
+		return
+	} else if len(pathSegment) >= 7 {
+		// /{{version}}/boards/{{board_id}}/treasures/{{treasures_id ... }}/articles
+		// or
+		// /{{version}}/boards/{{board_id}}/treasures/{{treasures_id ... }}/articles/{{filename}}
+		boardId = pathSegment[3]
+		if pathSegment[len(pathSegment)-1] == "articles" {
+			treasuresId = pathSegment[5 : len(pathSegment)-1]
+			filename = ""
+		} else {
+			treasuresId = pathSegment[5 : len(pathSegment)-2]
+			filename = pathSegment[len(pathSegment)-1]
+		}
+		return
+	}
+	// should not be reached
+	delivery.logger.Warningf("parseBoardTreasurePath got malform path: %v", path)
+	return
+}
+
+// parseClassPath covert url path from /v1/classes/1/information to
+// {1, information) or /v1/classes to {,}
+func (delivery *httpDelivery) parseClassPath(path string) (classId string, item string, err error) {
+	pathSegment := strings.Split(path, "/")
+	if len(pathSegment) == 5 {
+		// /{{version}}/classes/{{class_id}}/{{item}}
+		return pathSegment[3], pathSegment[4], nil
+	} else if len(pathSegment) == 4 {
+		// /{{version}}/classes/{{class_id}}
+		return pathSegment[3], "", nil
+	} else if len(pathSegment) == 3 {
+		// /{{version}}/classes
+		return "", "", nil
+	}
+	delivery.logger.Warningf("parseClassPath got malform path: %v", path)
+	return "", "", nil
+}
+
+func parseUserPath(path string) (userId string, item string, err error) {
+	pathSegment := strings.Split(path, "/")
+	// /{{version}}/users/{{user_id}}/{{item}}
+	if len(pathSegment) == 4 {
+		// /{{version}}/users/{{user_id}}
+		return pathSegment[3], "", nil
+	}
+	return pathSegment[3], pathSegment[4], nil
+}
+
+// return a boolean value to indicate support guest account
+// and using guest permission when permission insufficient
+func supportGuest() bool {
+	return false
+}
diff --git a/internal/delivery/http/route_boards.go b/internal/delivery/http/route_boards.go
new file mode 100644
index 0000000..253084c
--- /dev/null
+++ b/internal/delivery/http/route_boards.go
@@ -0,0 +1,99 @@
+package http
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+
+	"github.com/PichuChen/go-bbs"
+	"github.com/Ptt-official-app/Ptt-backend/internal/usecase"
+)
+
+func (delivery *httpDelivery) getBoardList(w http.ResponseWriter, r *http.Request) {
+	delivery.logger.Debugf("getBoardList: %v", r)
+
+	token := delivery.getTokenFromRequest(r)
+	userId, err := delivery.usecase.GetUserIdFromToken(token)
+	if err != nil {
+		// user permission error
+		// Support Guest?
+		if !supportGuest() {
+			w.WriteHeader(http.StatusUnauthorized)
+			w.Write([]byte(`{"error":"token_invalid"}`))
+			return
+		} else {
+			userId = "guest" // TODO: use const variable
+		}
+	}
+
+	boards := delivery.usecase.GetBoards(context.Background(), userId)
+
+	dataList := make([]interface{}, 0, len(boards))
+	for _, board := range boards {
+		dataList = append(dataList, marshalBoardHeader(board))
+	}
+
+	responseMap := map[string]interface{}{
+		"data": dataList,
+	}
+
+	b, _ := json.MarshalIndent(responseMap, "", "  ")
+	w.Write(b)
+}
+
+func (delivery *httpDelivery) getBoardInformation(w http.ResponseWriter, r *http.Request, boardId string) {
+	delivery.logger.Debugf("getBoardInformation: %v", r)
+	token := delivery.getTokenFromRequest(r)
+	err := delivery.usecase.CheckPermission(token,
+		[]usecase.Permission{usecase.PermissionReadBoardInformation},
+		map[string]string{
+			"board_id": boardId,
+		})
+
+	if err != nil {
+		// TODO: record unauthorized access
+		w.WriteHeader(http.StatusUnauthorized)
+		return
+	}
+
+	brd, err := delivery.usecase.GetBoardByID(context.Background(), boardId)
+	if err != nil {
+		// TODO: record error
+		delivery.logger.Warningf("find board %s failed: %v", boardId, err)
+		w.WriteHeader(http.StatusInternalServerError)
+		m := map[string]string{
+			"error":             "find_board_error",
+			"error_description": "get board for " + boardId + " failed",
+		}
+		b, _ := json.MarshalIndent(m, "", "  ")
+		w.Write(b)
+		return
+	}
+
+	responseMap := map[string]interface{}{
+		"data": marshalBoardHeader(brd),
+	}
+
+	b, _ := json.MarshalIndent(responseMap, "", "  ")
+	w.Write(b)
+}
+
+// marshal generate board or class metadata object,
+// b is input header
+func marshalBoardHeader(b bbs.BoardRecord) map[string]interface{} {
+	ret := map[string]interface{}{
+		"title":          b.Title(),
+		"number_of_user": "0",
+		"moderators":     b.BM(),
+	}
+	if b.IsClass() {
+		// class
+		// Assign ID from foreach loop
+		ret["type"] = "class"
+	} else {
+		// board
+		ret["id"] = b.BoardId()
+		ret["type"] = "board"
+	}
+	return ret
+}
diff --git a/internal/delivery/http/route_boards_treasures.go b/internal/delivery/http/route_boards_treasures.go
new file mode 100644
index 0000000..2bb1a0a
--- /dev/null
+++ b/internal/delivery/http/route_boards_treasures.go
@@ -0,0 +1,58 @@
+package http
+
+import (
+	"context"
+	// "encoding/base64"
+	"encoding/json"
+	"net/http"
+	"strings"
+
+	"github.com/Ptt-official-app/Ptt-backend/internal/usecase"
+)
+
+func (delivery *httpDelivery) getBoardTreasures(w http.ResponseWriter, r *http.Request, boardId string) {
+	delivery.logger.Debugf("getBoardTreasures: %v", r)
+	token := delivery.getTokenFromRequest(r)
+	_, treasuresId, filename, err := delivery.parseBoardTreasurePath(r.URL.Path)
+	if err != nil {
+		delivery.logger.Warningf("parseBoardTreasurePath error: %v", err)
+		// TODO return 400?
+	}
+	if filename != "" {
+		// get file
+		delivery.getBoardTreasuresFile(w, r, boardId, treasuresId, filename)
+		return
+	}
+
+	// Check permission for board
+	err = delivery.usecase.CheckPermission(token,
+		[]usecase.Permission{usecase.PermissionReadTreasureInformation},
+		map[string]string{
+			"board_id":    boardId,
+			"treasure_id": strings.Join(treasuresId, ","),
+		})
+	if err != nil {
+		// TODO: record unauthorized access
+		w.WriteHeader(http.StatusUnauthorized)
+		return
+	}
+
+	treasures := delivery.usecase.GetBoardTreasures(context.Background(), boardId, treasuresId)
+	delivery.logger.Debugf("fh: %v", treasures)
+
+	responseMap := map[string]interface{}{
+		"data": map[string]interface{}{
+			"items": treasures,
+		},
+	}
+
+	b, _ := json.MarshalIndent(responseMap, "", "  ")
+	w.Write(b)
+
+}
+
+func (delivery *httpDelivery) getBoardTreasuresFile(w http.ResponseWriter, r *http.Request, boardId string, treasuresId []string, filename string) {
+	delivery.logger.Debugf("getBoardTreasuresFile %v board: %v, treasuresId: %v, filename: %v", r, boardId, treasuresId, filename)
+
+	w.WriteHeader(http.StatusNotImplemented)
+}
diff --git a/internal/delivery/http/route_borads_articles.go b/internal/delivery/http/route_borads_articles.go
new file mode 100644
index 0000000..0f7a44b
--- /dev/null
+++ b/internal/delivery/http/route_borads_articles.go
@@ -0,0 +1,72 @@
+package http
+
+import (
+	"context"
+	"encoding/base64"
+	"encoding/json"
+	"net/http"
+
+	"github.com/Ptt-official-app/Ptt-backend/internal/usecase"
+)
+
+// getBoardArticles handles request with `/v1/boards/SYSOP/articles` and will return
+// article list to client
+func (delivery *httpDelivery) getBoardArticles(w http.ResponseWriter, r *http.Request, boardId string) {
+	delivery.logger.Debugf("getBoardArticles: %v", r)
+	token := delivery.getTokenFromRequest(r)
+	// Check permission for board
+	err := delivery.usecase.CheckPermission(token,
+		[]usecase.Permission{usecase.PermissionReadBoardInformation},
+		map[string]string{
+			"board_id": boardId,
+		})
+
+	if err != nil {
+		// TODO: record unauthorized access
+		w.WriteHeader(http.StatusUnauthorized)
+		return
+	}
+
+	responseMap := map[string]interface{}{
+		"data": map[string]interface{}{
+			"items": delivery.usecase.GetBoardArticles(context.Background(), boardId),
+		},
+	}
+
+	b, _ := json.MarshalIndent(responseMap, "", "  ")
+	w.Write(b)
+
+}
+
+func (delivery *httpDelivery) getBoardArticlesFile(w http.ResponseWriter, r *http.Request, boardId string, filename string) {
+	delivery.logger.Debugf("getBoardArticlesFile: %v", r)
+
+	token := delivery.getTokenFromRequest(r)
+	err := delivery.usecase.CheckPermission(token,
+		[]usecase.Permission{usecase.PermissionReadBoardInformation},
+		map[string]string{
+			"board_id": boardId,
+		})
+
+	if err != nil {
+		// TODO: record unauthorized access
+		w.WriteHeader(http.StatusUnauthorized)
+		return
+	}
+
+	buf, err := delivery.usecase.GetBoardArticle(context.Background(), boardId, filename)
+	if err != nil {
+		delivery.logger.Errorf("failed to get board article: %s", err)
+	}
+
+	bufStr := base64.StdEncoding.EncodeToString(buf)
+
+	responseMap := map[string]interface{}{
+		"data": map[string]interface{}{
+			"raw": bufStr,
+		},
+	}
+
+	b, _ := json.MarshalIndent(responseMap, "", "  ")
+	w.Write(b)
+}
diff --git a/internal/delivery/http/route_classes.go b/internal/delivery/http/route_classes.go
new file mode 100644
index 0000000..0fa8243
--- /dev/null
+++ b/internal/delivery/http/route_classes.go
@@ -0,0 +1,112 @@
+package http
+
+import (
+	"context"
+	// "github.com/PichuChen/go-bbs"
+	// "github.com/PichuChen/go-bbs/crypt"
+	// "log"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strings"
+)
+
+func (delivery *httpDelivery) getClass(w http.ResponseWriter, r *http.Request) {
+
+	seg := strings.Split(r.URL.Path, "/")
+
+	classId := "0"
+	if len(seg) > 2 {
+		classId = seg[3]
+	}
+	delivery.logger.Informationalf("user get class: %v", classId)
+
+	list := []interface{}{}
+
+	c := map[string]interface{}{
+		"id":             1,
+		"type":           "class",
+		"title":          "title",
+		"number_of_user": 3,
+		"moderators": []string{
+			"SYSOP",
+			"pichu",
+		},
+	}
+	list = append(list, c)
+
+	m := map[string]interface{}{
+		"data": list,
+	}
+	b, _ := json.MarshalIndent(m, "", "  ")
+
+	w.Write(b)
+
+}
+
+// getClasses HandleFunc handles path start with `/v1/classes`
+// and pass requests to next handle function
+func (delivery *httpDelivery) getClasses(w http.ResponseWriter, r *http.Request) {
+	delivery.logger.Debugf("getClasses: %v", r)
+	classId, item, err := delivery.parseClassPath(r.URL.Path)
+	delivery.logger.Noticef("query class: %v item: %v err: %v", classId, item, err)
+	if classId == "" {
+		getClassesWithoutClassId(w, r)
+		return
+	}
+	delivery.getClassesList(w, r, classId)
+	return
+
+	// // get single board
+	// if item == "information" {
+	// 	getBoardInformation(w, r, boardId)
+	// 	return
+	// }
+
+}
+
+// getClassesWithoutClassId handles path don't contain item after class id
+// eg: `/v1/classes`, it will redirect Client to `/v1/classes/1` which is
+// root class by default.
+func getClassesWithoutClassId(w http.ResponseWriter, r *http.Request) {
+	http.Redirect(w, r, "/v1/classes/1", 301)
+}
+
+// getClassesList handle path with class id and will return boards and classes
+// under this class.
+// TODO: What should we return when target class not found?
+func (delivery *httpDelivery) getClassesList(w http.ResponseWriter, r *http.Request, classId string) {
+	delivery.logger.Debugf("getClassesList: %v", r)
+
+	token := delivery.getTokenFromRequest(r)
+	userId, err := delivery.usecase.GetUserIdFromToken(token)
+	if err != nil {
+		// user permission error
+		// Support Guest?
+		if !supportGuest() {
+			w.WriteHeader(http.StatusUnauthorized)
+			w.Write([]byte(`{"error":"token_invalid"}`))
+			return
+		} else {
+			userId = "guest" // TODO: use const variable
+		}
+	}
+
+	boards := delivery.usecase.GetClasses(context.Background(), userId, classId)
+
+	dataList := []interface{}{}
+	for bid, b := range boards {
+		m := marshalBoardHeader(b)
+		if b.IsClass() {
+			m["id"] = fmt.Sprintf("%v", bid+1)
+		}
+		dataList = append(dataList, m)
+	}
+
+	responseMap := map[string]interface{}{
+		"data": dataList,
+	}
+
+	b, _ := json.MarshalIndent(responseMap, "", "  ")
+	w.Write(b)
+}
diff --git a/route_token.go b/internal/delivery/http/route_token.go
similarity index 58%
rename from route_token.go
rename to internal/delivery/http/route_token.go
index 0d26ea8..bb5dcbb 100644
--- a/route_token.go
+++ b/internal/delivery/http/route_token.go
@@ -1,33 +1,23 @@
-package main
+package http
 
 import (
+	"context"
 	"encoding/json"
-	"fmt"
-	"github.com/PichuChen/go-bbs"
-
 	"log"
 	"net/http"
 	"strings"
-)
 
-func routeToken(w http.ResponseWriter, r *http.Request) {
-	// TODO: Check IP Flowspeed
-
-	if r.Method == "POST" {
-		postToken(w, r)
-		return
-	}
-
-}
+	"github.com/PichuChen/go-bbs"
+)
 
-func postToken(w http.ResponseWriter, r *http.Request) {
+func (delivery *httpDelivery) postToken(w http.ResponseWriter, r *http.Request) {
 
 	r.ParseForm()
 
 	username := r.FormValue("username")
 	password := r.FormValue("password")
 
-	userec, err := findUserecById(username)
+	userec, err := delivery.usecase.GetUserByID(context.Background(), username)
 	if err != nil {
 		m := map[string]string{
 			"error":             "grant_error",
@@ -40,7 +30,7 @@ func postToken(w http.ResponseWriter, r *http.Request) {
 	}
 
 	log.Println("found user:", userec)
-	err = verifyPassword(userec, password)
+	err = delivery.verifyPassword(userec, password)
 	if err != nil {
 		// TODO: add delay, warning, notify user
 
@@ -55,7 +45,7 @@ func postToken(w http.ResponseWriter, r *http.Request) {
 	}
 
 	// Generate Access Token
-	token := newAccessTokenWithUsername(username)
+	token := delivery.usecase.CreateAccessTokenWithUsername(username)
 	m := map[string]string{
 		"access_token": token,
 		"token_type":   "bearer",
@@ -64,30 +54,18 @@ func postToken(w http.ResponseWriter, r *http.Request) {
 	b, _ := json.MarshalIndent(m, "", "  ")
 
 	w.Write(b)
-
-}
-
-func findUserecById(userid string) (bbs.UserRecord, error) {
-
-	for _, it := range userRecs {
-		if userid == it.UserId() {
-			return it, nil
-		}
-	}
-	return nil, fmt.Errorf("user record not found")
-
 }
 
-func verifyPassword(userec bbs.UserRecord, password string) error {
+func (delivery *httpDelivery) verifyPassword(userec bbs.UserRecord, password string) error {
 	log.Println("password", userec.HashedPassword())
 	return userec.VerifyPassword(password)
 }
 
-func getTokenFromRequest(r *http.Request) string {
+func (delivery *httpDelivery) getTokenFromRequest(r *http.Request) string {
 	a := r.Header.Get("Authorization")
 	s := strings.Split(a, " ")
 	if len(s) < 2 {
-		logger.Warningf("getTokenFromRequest error: len(s) < 2, got: %v", len(s))
+		delivery.logger.Warningf("getTokenFromRequest error: len(s) < 2, got: %v", len(s))
 		return ""
 	}
 	return s[1]
diff --git a/internal/delivery/http/route_users.go b/internal/delivery/http/route_users.go
new file mode 100644
index 0000000..f3295c9
--- /dev/null
+++ b/internal/delivery/http/route_users.go
@@ -0,0 +1,88 @@
+package http
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+
+	"github.com/Ptt-official-app/Ptt-backend/internal/usecase"
+)
+
+func (delivery *httpDelivery) getUsers(w http.ResponseWriter, r *http.Request) {
+	userId, item, err := parseUserPath(r.URL.Path)
+	switch item {
+	case "information":
+		delivery.getUserInformation(w, r, userId)
+	case "favorites":
+		delivery.getUserFavorites(w, r, userId)
+	default:
+		delivery.logger.Noticef("user id: %v not exist but be queried, info: %v err: %v", userId, item, err)
+		w.WriteHeader(http.StatusNotFound)
+	}
+}
+
+func (delivery *httpDelivery) getUserInformation(w http.ResponseWriter, r *http.Request, userId string) {
+	token := delivery.getTokenFromRequest(r)
+
+	err := delivery.usecase.CheckPermission(token,
+		[]usecase.Permission{usecase.PermissionReadUserInformation},
+		map[string]string{
+			"user_id": userId,
+		})
+
+	if err != nil {
+		// TODO: record unauthorized access
+		w.WriteHeader(http.StatusUnauthorized)
+		return
+	}
+
+	dataMap, err := delivery.usecase.GetUserInformation(context.Background(), userId)
+	if err != nil {
+		// TODO: record error
+		w.WriteHeader(http.StatusInternalServerError)
+		m := map[string]string{
+			"error":             "find_userrec_error",
+			"error_description": err.Error(),
+		}
+		b, _ := json.MarshalIndent(m, "", "  ")
+		w.Write(b)
+		return
+	}
+
+	responseMap := map[string]interface{}{
+		"data": dataMap,
+	}
+	responseByte, _ := json.MarshalIndent(responseMap, "", "  ")
+
+	w.Write(responseByte)
+}
+
+func (delivery *httpDelivery) getUserFavorites(w http.ResponseWriter, r *http.Request, userId string) {
+	token := delivery.getTokenFromRequest(r)
+	err := delivery.usecase.CheckPermission(token,
+		[]usecase.Permission{usecase.PermissionReadUserInformation},
+		map[string]string{
+			"user_id": userId,
+		})
+
+	if err != nil {
+		// TODO: record unauthorized access
+		w.WriteHeader(http.StatusUnauthorized)
+		return
+	}
+
+	dataItems, err := delivery.usecase.GetUserFavorites(context.Background(), userId)
+	if err != nil {
+		delivery.logger.Errorf("failed to get user favorites: %s\n", err)
+	}
+
+	responseMap := map[string]interface{}{
+		"data": map[string]interface{}{
+			"items": dataItems,
+		},
+	}
+
+	responseByte, _ := json.MarshalIndent(responseMap, "", "  ")
+
+	w.Write(responseByte)
+}
diff --git a/internal/mock/repository.go b/internal/mock/repository.go
new file mode 100644
index 0000000..906f89a
--- /dev/null
+++ b/internal/mock/repository.go
@@ -0,0 +1,39 @@
+package mock
+
+import (
+	"context"
+
+	"github.com/PichuChen/go-bbs"
+	"github.com/Ptt-official-app/Ptt-backend/internal/repository"
+)
+
+// implements repository.Repository
+type MockRepository struct {
+}
+
+func NewMockRepository() repository.Repository {
+	return &MockRepository{}
+}
+
+// repository/board.go
+func (repo *MockRepository) GetBoards(ctx context.Context) []bbs.BoardRecord {
+	panic("Not implemented")
+}
+func (repo *MockRepository) GetBoardArticle(ctx context.Context, boardID, filename string) ([]byte, error) {
+	panic("Not implemented")
+}
+func (repo *MockRepository) GetBoardArticleRecords(ctx context.Context, boardID string) ([]bbs.ArticleRecord, error) {
+	panic("Not implemented")
+}
+
+func (repo *MockRepository) GetBoardTreasureRecords(ctx context.Context, boardID string, treasureIDs []string) ([]bbs.ArticleRecord, error) {
+	panic("Not implemented")
+}
+
+// repository/user.go
+func (repo *MockRepository) GetUsers(ctx context.Context) []bbs.UserRecord {
+	panic("Not implemented")
+}
+func (repo *MockRepository) GetUserFavoriteRecords(ctx context.Context, userID string) ([]bbs.FavoriteRecord, error) {
+	panic("Not implemented")
+}
diff --git a/internal/mock/usecase.go b/internal/mock/usecase.go
new file mode 100644
index 0000000..60649f1
--- /dev/null
+++ b/internal/mock/usecase.go
@@ -0,0 +1,70 @@
+package mock
+
+import (
+	"context"
+
+	"github.com/PichuChen/go-bbs"
+	"github.com/Ptt-official-app/Ptt-backend/internal/usecase"
+)
+
+// implements usecase.Usecase
+type MockUsecase struct {
+}
+
+func NewMockUsecase() usecase.Usecase {
+	return &MockUsecase{}
+}
+
+// usecase/user.go
+func (usecase *MockUsecase) GetUserByID(ctx context.Context, userID string) (bbs.UserRecord, error) {
+	panic("Not implemented")
+}
+
+func (usecase *MockUsecase) GetUserFavorites(ctx context.Context, userID string) ([]interface{}, error) {
+	panic("Not implemented")
+}
+
+func (usecase *MockUsecase) GetUserInformation(ctx context.Context, userID string) (map[string]interface{}, error) {
+	result := map[string]interface{}{
+		"user_id": "id",
+	}
+	return result, nil
+}
+
+// usecase/board.go
+func (usecase *MockUsecase) GetBoardByID(ctx context.Context, boardID string) (bbs.BoardRecord, error) {
+	panic("Not implemented")
+}
+
+func (usecase *MockUsecase) GetBoards(ctx context.Context, userID string) []bbs.BoardRecord {
+	panic("Not implemented")
+}
+
+func (usecase *MockUsecase) GetClasses(ctx context.Context, userID, classID string) []bbs.BoardRecord {
+	panic("Not implemented")
+}
+
+func (usecase *MockUsecase) GetBoardArticles(ctx context.Context, boardID string) []interface{} {
+	panic("Not implemented")
+}
+
+func (usecase *MockUsecase) GetBoardArticle(ctx context.Context, boardID, filename string) ([]byte, error) {
+	panic("Not implemented")
+}
+
+func (usecase *MockUsecase) GetBoardTreasures(ctx context.Context, boardID string, treasuresID []string) []interface{} {
+	panic("Not implemented")
+}
+
+// usecase/token.go
+func (usecase *MockUsecase) CreateAccessTokenWithUsername(username string) string {
+	return "token"
+}
+
+func (usecase *MockUsecase) GetUserIdFromToken(token string) (string, error) {
+	panic("Not implemented")
+}
+
+func (usecase *MockUsecase) CheckPermission(token string, permissionId []usecase.Permission, userInfo map[string]string) error {
+	return nil
+}
diff --git a/internal/repository/board.go b/internal/repository/board.go
new file mode 100644
index 0000000..802b4a8
--- /dev/null
+++ b/internal/repository/board.go
@@ -0,0 +1,36 @@
+package repository
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/PichuChen/go-bbs"
+)
+
+func (repo *repository) GetBoards(_ context.Context) []bbs.BoardRecord {
+	return repo.boardRecords
+}
+
+func (repo *repository) GetBoardArticle(_ context.Context, boardID, filename string) ([]byte, error) {
+	return repo.db.ReadBoardArticleFile(boardID, filename)
+}
+
+func (repo *repository) GetBoardArticleRecords(_ context.Context, boardID string) ([]bbs.ArticleRecord, error) {
+	return repo.db.ReadBoardArticleRecordsFile(boardID)
+}
+
+func (repo *repository) GetBoardTreasureRecords(_ context.Context, boardID string, treasureIDs []string) ([]bbs.ArticleRecord, error) {
+	return repo.db.ReadBoardTreasureRecordsFile(boardID, treasureIDs)
+}
+
+func loadBoardFile(db *bbs.DB) ([]bbs.BoardRecord, error) {
+	boardRecords, err := db.ReadBoardRecords()
+	if err != nil {
+		logger.Errorf("get board header error: %v", err)
+		return nil, fmt.Errorf("failed to read board records: %w", err)
+	}
+	for index, board := range boardRecords {
+		logger.Debugf("loaded %d %v", index, board.BoardId())
+	}
+	return boardRecords, nil
+}
diff --git a/internal/repository/repository.go b/internal/repository/repository.go
new file mode 100644
index 0000000..1a9bf0e
--- /dev/null
+++ b/internal/repository/repository.go
@@ -0,0 +1,57 @@
+package repository
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/PichuChen/go-bbs"
+	"github.com/Ptt-official-app/Ptt-backend/internal/logging"
+)
+
+var (
+	logger = logging.NewLogger()
+)
+
+// Repository directly interacts with database via db handler.
+type Repository interface {
+
+	// board.go
+	// GetBoards return all board record
+	GetBoards(ctx context.Context) []bbs.BoardRecord
+	// GetBoardArticle returns an article file in a specified board and filename
+	GetBoardArticle(ctx context.Context, boardID, filename string) ([]byte, error)
+	// GetBoardArticleRecords returns article records of a board
+	GetBoardArticleRecords(ctx context.Context, boardID string) ([]bbs.ArticleRecord, error)
+	// GetBoardTreasureRecords returns treasure article records of a board
+	GetBoardTreasureRecords(ctx context.Context, boardID string, treasureIDs []string) ([]bbs.ArticleRecord, error)
+
+	// user.go
+	// GetUsers returns all user reords
+	GetUsers(ctx context.Context) []bbs.UserRecord
+	// GetUserFavoriteRecords returns favorite records of a user
+	GetUserFavoriteRecords(ctx context.Context, userID string) ([]bbs.FavoriteRecord, error)
+}
+
+type repository struct {
+	db           *bbs.DB
+	userRecords  []bbs.UserRecord
+	boardRecords []bbs.BoardRecord
+}
+
+func NewRepository(db *bbs.DB) (Repository, error) {
+	userRecords, err := loadUserRecords(db)
+	if err != nil {
+		return nil, fmt.Errorf("failed to load user records: %w", err)
+	}
+
+	boardRecords, err := loadBoardFile(db)
+	if err != nil {
+		return nil, fmt.Errorf("failed to load board file: %w", err)
+	}
+
+	return &repository{
+		db:           db,
+		boardRecords: boardRecords,
+		userRecords:  userRecords,
+	}, nil
+}
diff --git a/internal/repository/user.go b/internal/repository/user.go
new file mode 100644
index 0000000..da1b3ac
--- /dev/null
+++ b/internal/repository/user.go
@@ -0,0 +1,25 @@
+package repository
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/PichuChen/go-bbs"
+)
+
+func (repo *repository) GetUsers(_ context.Context) []bbs.UserRecord {
+	return repo.userRecords
+}
+
+func (repo *repository) GetUserFavoriteRecords(ctx context.Context, userID string) ([]bbs.FavoriteRecord, error) {
+	return repo.db.ReadUserFavoriteRecords(userID)
+}
+
+func loadUserRecords(db *bbs.DB) ([]bbs.UserRecord, error) {
+	userRecords, err := db.ReadUserRecords()
+	if err != nil {
+		logger.Errorf("get user rec error: %v", err)
+		return nil, fmt.Errorf("failed to read user records: %w", err)
+	}
+	return userRecords, nil
+}
diff --git a/internal/usecase/board.go b/internal/usecase/board.go
new file mode 100644
index 0000000..d2d9f55
--- /dev/null
+++ b/internal/usecase/board.go
@@ -0,0 +1,120 @@
+package usecase
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/PichuChen/go-bbs"
+)
+
+func (usecase *usecase) GetBoardByID(ctx context.Context, boardID string) (bbs.BoardRecord, error) {
+	for _, it := range usecase.repo.GetBoards(ctx) {
+		if boardID == it.BoardId() {
+			return it, nil
+		}
+	}
+	return nil, fmt.Errorf("board record not found")
+}
+
+func (usecase *usecase) GetBoards(ctx context.Context, userID string) []bbs.BoardRecord {
+	boards := make([]bbs.BoardRecord, 0)
+	for _, board := range usecase.repo.GetBoards(ctx) {
+		// TODO: Show Board by user level
+		if board.IsClass() {
+			continue
+		}
+		if !usecase.shouldShowOnUserLevel(board, userID) {
+			continue
+		}
+		boards = append(boards, board)
+	}
+	return boards
+}
+
+func (usecase *usecase) GetClasses(ctx context.Context, userID, classID string) []bbs.BoardRecord {
+	boards := make([]bbs.BoardRecord, 0)
+	for _, board := range usecase.repo.GetBoards(ctx) {
+		// TODO: Show Board by user level
+		if !usecase.shouldShowOnUserLevel(board, userID) {
+			continue
+		}
+		if board.ClassId() != classID {
+			continue
+		}
+		// m := marshalBoardHeader(board)
+		// if board.IsClass() {
+		// 	m["id"] = fmt.Sprintf("%v", bid+1)
+		// }
+		boards = append(boards, board)
+	}
+	return boards
+}
+
+func (usecase *usecase) GetBoardArticles(ctx context.Context, boardID string) []interface{} {
+	articleRecords, err := usecase.repo.GetBoardArticleRecords(ctx, boardID)
+	if err != nil {
+		usecase.logger.Warningf("open directory file error: %v", err)
+		// The board may not contain any article
+	}
+
+	items := []interface{}{}
+	for _, f := range articleRecords {
+		m := map[string]interface{}{
+			"filename": f.Filename(),
+			// Bug(pichu): f.Modified time will be 0 when file is vote
+			"modified_time":   f.Modified(),
+			"recommend_count": f.Recommend(),
+			"post_date":       f.Date(),
+			"title":           f.Title(),
+			"money":           fmt.Sprintf("%v", f.Money()),
+			"owner":           f.Owner(),
+			// "aid": ""
+			"url": getArticleURL(boardID, f.Filename()),
+		}
+		items = append(items, m)
+	}
+	return items
+}
+
+func (usecase *usecase) GetBoardArticle(ctx context.Context, boardID, filename string) ([]byte, error) {
+	buf, err := usecase.repo.GetBoardArticle(ctx, boardID, filename)
+	if err != nil {
+		return nil, fmt.Errorf("read file %s/%s error: %w", boardID, filename, err)
+	}
+	return buf, nil
+}
+
+func (usecase *usecase) GetBoardTreasures(ctx context.Context, boardID string, treasuresID []string) []interface{} {
+	fileHeaders, err := usecase.repo.GetBoardTreasureRecords(ctx, boardID, treasuresID)
+	if err != nil {
+		usecase.logger.Warningf("open directory file error: %v", err)
+		// The board may not contain any article
+	}
+
+	items := []interface{}{}
+	for _, f := range fileHeaders {
+		m := map[string]interface{}{
+			"filename": f.Filename(),
+			// Bug(pichu): f.Modified time will be 0 when file is vote
+			"modified_time":   f.Modified(),
+			"recommend_count": f.Recommend(),
+			"post_date":       f.Date(),
+			"title":           f.Title(),
+			"money":           fmt.Sprintf("%v", f.Money()),
+			"owner":           f.Owner(),
+			// "aid": ""
+			"url": getArticleURL(boardID, f.Filename()),
+		}
+		items = append(items, m)
+	}
+	return items
+}
+
+func (usecase *usecase) shouldShowOnUserLevel(board bbs.BoardRecord, userID string) bool {
+	// TODO: Get user Level
+	return true
+}
+
+func getArticleURL(boardId string, filename string) string {
+	return fmt.Sprintf("https://ptt-app-dev-codingman.pichuchen.tw/bbs/%s/%s.html", boardId, filename)
+}
diff --git a/internal/usecase/token.go b/internal/usecase/token.go
new file mode 100644
index 0000000..c1c36cb
--- /dev/null
+++ b/internal/usecase/token.go
@@ -0,0 +1,80 @@
+package usecase
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/dgrijalva/jwt-go"
+)
+
+type Permission string
+
+const (
+	PermissionReadUserInformation     Permission = "READ_USER_INFORMATION"
+	PermissionReadBoardInformation    Permission = "READ_BOARD_INFORMATION"
+	PermissionReadTreasureInformation Permission = "READ_TREASURE_INFORMATION"
+	PermissionReadFavorite            Permission = "READ_FAVORITE"
+)
+
+func (usecase *usecase) CreateAccessTokenWithUsername(username string) string {
+	claims := &jwt.StandardClaims{
+		ExpiresAt: time.Now().Add(usecase.globalConfig.AccessTokenExpiresAt).Unix(),
+		// Issuer:    "test",
+		Subject: username,
+	}
+
+	// TODO: Setting me in config
+	// openssl ecparam -name prime256v1 -genkey -noout -out pkey
+	privateKey := usecase.globalConfig.AccessTokenPrivateKey
+
+	key, err := jwt.ParseECPrivateKeyFromPEM([]byte(privateKey))
+	if err != nil {
+		usecase.logger.Criticalf("parse private key failed: %v", err)
+		return ""
+	}
+
+	token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
+	ss, err := token.SignedString(key)
+	if err != nil {
+		usecase.logger.Errorf("sign token failed: %v", err)
+		return ""
+	}
+	return ss
+}
+
+func (t *usecase) GetUserIdFromToken(token string) (string, error) {
+	t.logger.Debugf("getUserIdFromToken token: %v", token)
+	pem := t.globalConfig.AccessTokenPublicKey
+	key, err := jwt.ParseECPublicKeyFromPEM([]byte(pem))
+	if err != nil {
+		t.logger.Criticalf("parse public key failed: %v", err)
+		return "", err
+	}
+
+	jwtToken, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{},
+		func(token *jwt.Token) (i interface{}, e error) {
+			return key, nil
+		})
+	if err != nil {
+		t.logger.Warningf("parse token failed: %v", err)
+		return "", err
+	}
+
+	if jwtToken == nil {
+		t.logger.Warningf("jwtToken == nil")
+		return "", nil
+	}
+
+	// logger.Debugf("getUserIdFromToken jwtToken: %v %v", jwtToken, err)
+	if claim, ok := jwtToken.Claims.(*jwt.StandardClaims); ok && jwtToken.Valid {
+		t.logger.Debugf("subject: %v %v", claim, jwtToken.Valid)
+		return claim.Subject, nil
+		// return "", nil
+	}
+	t.logger.Debugf("subject: %v", jwtToken.Valid)
+	return "", fmt.Errorf("token not valid")
+}
+
+func (usecase *usecase) CheckPermission(token string, permissionId []Permission, userInfo map[string]string) error {
+	return nil
+}
diff --git a/internal/usecase/usecase.go b/internal/usecase/usecase.go
new file mode 100644
index 0000000..4b096aa
--- /dev/null
+++ b/internal/usecase/usecase.go
@@ -0,0 +1,57 @@
+package usecase
+
+import (
+	"context"
+
+	"github.com/PichuChen/go-bbs"
+	"github.com/Ptt-official-app/Ptt-backend/internal/config"
+	"github.com/Ptt-official-app/Ptt-backend/internal/logging"
+	"github.com/Ptt-official-app/Ptt-backend/internal/repository"
+)
+
+// Usecase is the implementation of backend business logic.
+type Usecase interface {
+	// user.go
+	// GetUserByID returns a user of userID
+	GetUserByID(ctx context.Context, userID string) (bbs.UserRecord, error)
+	// GetUserFavorites returns favorite records of a user
+	GetUserFavorites(ctx context.Context, userID string) ([]interface{}, error) // FIXME: use concrete type rather than []interface{}
+	// GetUserInformation returns user info of a user
+	GetUserInformation(ctx context.Context, userID string) (map[string]interface{}, error) // FIXME: use concrete type rather than map[string]interface{}
+
+	// board.go
+	// GetBoardByID returns board record of board id
+	GetBoardByID(ctx context.Context, boardID string) (bbs.BoardRecord, error)
+	// GetBoards returns all board records
+	GetBoards(ctx context.Context, userID string) []bbs.BoardRecord
+	// GetClasses returns board records in a class
+	GetClasses(ctx context.Context, userID, classID string) []bbs.BoardRecord
+	// GetBoardArticles returns articles of a board
+	GetBoardArticles(ctx context.Context, boardID string) []interface{} // FIXME: use concrete type rather than []interface{}
+	// GetBoardArticle returns an article file given board id and file name
+	GetBoardArticle(ctx context.Context, boardID, filename string) ([]byte, error)
+	// GetBoardTreasures returns treasures of a board
+	GetBoardTreasures(ctx context.Context, boardID string, treasuresID []string) []interface{} // FIXME: use concrete type rather than []interface{}
+
+	// token.go
+	// CreateAccessTokenWithUsername creates access token for a user
+	CreateAccessTokenWithUsername(username string) string
+	// GetUserIdFromToken retrieves user id by token
+	GetUserIdFromToken(token string) (string, error)
+	// CheckPermission checks permissions
+	CheckPermission(token string, permissionId []Permission, userInfo map[string]string) error // FIXME: use concrete type rather than map[string]string
+}
+
+type usecase struct {
+	logger       logging.Logger
+	globalConfig *config.Config
+	repo         repository.Repository
+}
+
+func NewUsecase(globalConfig *config.Config, repo repository.Repository) Usecase {
+	return &usecase{
+		logger:       logging.NewLogger(),
+		globalConfig: globalConfig,
+		repo:         repo,
+	}
+}
diff --git a/internal/usecase/user.go b/internal/usecase/user.go
new file mode 100644
index 0000000..25fdc78
--- /dev/null
+++ b/internal/usecase/user.go
@@ -0,0 +1,84 @@
+package usecase
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/PichuChen/go-bbs"
+)
+
+func (usecase *usecase) GetUserByID(ctx context.Context, userID string) (bbs.UserRecord, error) {
+	for _, it := range usecase.repo.GetUsers(ctx) {
+		if userID == it.UserId() {
+			return it, nil
+		}
+	}
+	return nil, fmt.Errorf("user record not found")
+}
+
+func (usecase *usecase) GetUserFavorites(ctx context.Context, userID string) ([]interface{}, error) {
+	recs, err := usecase.repo.GetUserFavoriteRecords(ctx, userID)
+	if err != nil {
+		return nil, err
+	}
+
+	dataItems := usecase.parseFavoriteFolderItem(recs)
+	return dataItems, nil
+}
+
+func (usecase *usecase) GetUserInformation(ctx context.Context, userID string) (map[string]interface{}, error) {
+	user, err := usecase.GetUserByID(ctx, userID)
+	if err != nil {
+		return nil, fmt.Errorf("get userrec for %s failed", userID)
+	}
+
+	// TODO: Check Etag or Not-Modified for cache
+
+	result := map[string]interface{}{
+		"user_id":              user.UserId(),
+		"nickname":             user.Nickname(),
+		"realname":             user.RealName(),
+		"number_of_login_days": fmt.Sprintf("%d", user.NumLoginDays()),
+		"number_of_posts":      fmt.Sprintf("%d", user.NumPosts()),
+		// "number_of_badposts":   fmt.Sprintf("%d", user.NumLoginDays),
+		"money":           fmt.Sprintf("%d", user.Money()),
+		"last_login_time": user.LastLogin().Format(time.RFC3339),
+		"last_login_ipv4": user.LastHost(),
+		"last_login_ip":   user.LastHost(),
+		// "last_login_country": fmt.Sprintf("%d", user.NumLoginDays),
+		"chess_status": map[string]interface{}{},
+		"plan":         map[string]interface{}{},
+	}
+	return result, nil
+}
+
+func (usecase *usecase) parseFavoriteFolderItem(recs []bbs.FavoriteRecord) []interface{} {
+	dataItems := []interface{}{}
+	for _, item := range recs {
+		usecase.logger.Debugf("fav type: %v", item.Type())
+
+		switch item.Type() {
+		case bbs.FavoriteTypeBoard:
+			dataItems = append(dataItems, map[string]interface{}{
+				"type":     "board",
+				"board_id": item.BoardId(),
+			})
+
+		case bbs.FavoriteTypeFolder:
+			dataItems = append(dataItems, map[string]interface{}{
+				"type":  "folder",
+				"title": item.Title(),
+				"items": usecase.parseFavoriteFolderItem(item.Records()),
+			})
+
+		case bbs.FavoriteTypeLine:
+			dataItems = append(dataItems, map[string]interface{}{
+				"type": "line",
+			})
+		default:
+			usecase.logger.Warningf("parseFavoriteFolderItem unknown favItem type")
+		}
+	}
+	return dataItems
+}
diff --git a/main.go b/main.go
deleted file mode 100644
index 72a455c..0000000
--- a/main.go
+++ /dev/null
@@ -1,111 +0,0 @@
-package main
-
-import (
-	"encoding/json"
-	"fmt"
-	"net/http"
-	"strings"
-
-	"github.com/PichuChen/go-bbs"
-	_ "github.com/PichuChen/go-bbs/pttbbs"
-	"github.com/Ptt-official-app/Ptt-backend/internal/logging"
-)
-
-var userRecs []bbs.UserRecord
-var boardHeader []bbs.BoardRecord
-
-var db *bbs.DB
-var logger = logging.NewLogger()
-
-func main() {
-	logger.Informationalf("server start")
-
-	loadDefaultConfig()
-	var err error
-	db, err = bbs.Open("pttbbs", globalConfig.BBSHome)
-	if err != nil {
-		logger.Errorf("open bbs db error: %v", err)
-		return
-	}
-
-	loadPasswdsFile()
-	loadBoardFile()
-
-	r := http.NewServeMux()
-	buildRoute(r)
-
-	logger.Informationalf("listen port on %v", globalConfig.ListenPort)
-	err = http.ListenAndServe(fmt.Sprintf(":%v", globalConfig.ListenPort), r)
-	if err != nil {
-		logger.Errorf("listen serve error: %v", err)
-	}
-}
-
-func loadPasswdsFile() {
-	var err error
-	userRecs, err = db.ReadUserRecords()
-	if err != nil {
-		logger.Errorf("get user rec error: %v", err)
-		return
-	}
-}
-
-func loadBoardFile() {
-	var err error
-	boardHeader, err = db.ReadBoardRecords()
-	if err != nil {
-		logger.Errorf("get board header error: %v", err)
-		return
-	}
-	for index, board := range boardHeader {
-		logger.Debugf("loaded %d %v", index, board.BoardId())
-
-	}
-}
-
-func routeClass(w http.ResponseWriter, r *http.Request) {
-	if r.Method == "GET" {
-		getClass(w, r)
-		return
-	}
-
-}
-
-func getClass(w http.ResponseWriter, r *http.Request) {
-
-	seg := strings.Split(r.URL.Path, "/")
-
-	classId := "0"
-	if len(seg) > 2 {
-		classId = seg[3]
-	}
-	logger.Informationalf("user get class: %v", classId)
-
-	list := []interface{}{}
-
-	c := map[string]interface{}{
-		"id":             1,
-		"type":           "class",
-		"title":          "title",
-		"number_of_user": 3,
-		"moderators": []string{
-			"SYSOP",
-			"pichu",
-		},
-	}
-	list = append(list, c)
-
-	m := map[string]interface{}{
-		"data": list,
-	}
-	b, _ := json.MarshalIndent(m, "", "  ")
-
-	w.Write(b)
-
-}
-
-// return a boolean value to indicate support guest account
-// and using guset permission when permission insufficient
-func supportGuest() bool {
-	return false
-}
diff --git a/permissions.go b/permissions.go
deleted file mode 100644
index 68cee33..0000000
--- a/permissions.go
+++ /dev/null
@@ -1,81 +0,0 @@
-package main
-
-import (
-	"github.com/dgrijalva/jwt-go"
-
-	"fmt"
-	"time"
-)
-
-type permission string
-
-const (
-	PermissionReadUserInformation     permission = "READ_USER_INFORMATION"
-	PermissionReadBoardInformation    permission = "READ_BOARD_INFORMATION"
-	PermissionReadTreasureInformation permission = "READ_TREASURE_INFORMATION"
-	PermissionReadFavorite            permission = "READ_FAVORITE"
-)
-
-func checkTokenPermission(token string, permissionId []permission, userInfo map[string]string) error {
-	return nil
-}
-
-func newAccessTokenWithUsername(username string) string {
-	claims := &jwt.StandardClaims{
-		ExpiresAt: time.Now().Add(globalConfig.AccessTokenExpiresAt).Unix(),
-		// Issuer:    "test",
-		Subject: username,
-	}
-
-	// TODO: Setting me in config
-	// openssl ecparam -name prime256v1 -genkey -noout -out pkey
-	privateKey := globalConfig.AccessTokenPrivateKey
-
-	key, err := jwt.ParseECPrivateKeyFromPEM([]byte(privateKey))
-	if err != nil {
-		logger.Criticalf("parse private key failed: %v", err)
-		return ""
-	}
-
-	token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
-	ss, err := token.SignedString(key)
-	if err != nil {
-		logger.Errorf("sign token failed: %v", err)
-		return ""
-	}
-	return ss
-}
-
-func getUserIdFromToken(token string) (string, error) {
-	logger.Debugf("getUserIdFromToken token: %v", token)
-	pem := globalConfig.AccessTokenPublicKey
-	key, err := jwt.ParseECPublicKeyFromPEM([]byte(pem))
-	if err != nil {
-		logger.Criticalf("parse public key failed: %v", err)
-		return "", err
-	}
-
-	jwtToken, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{},
-		func(token *jwt.Token) (i interface{}, e error) {
-			return key, nil
-		})
-	if err != nil {
-		logger.Warningf("parse token failed: %v", err)
-		return "", err
-	}
-
-	if jwtToken == nil {
-		logger.Warningf("jwtToken == nil")
-		return "", nil
-	}
-
-	// logger.Debugf("getUserIdFromToken jwtToken: %v %v", jwtToken, err)
-	if claim, ok := jwtToken.Claims.(*jwt.StandardClaims); ok && jwtToken.Valid {
-		logger.Debugf("subject: %v %v", claim, jwtToken.Valid)
-		return claim.Subject, nil
-		// return "", nil
-	}
-	logger.Debugf("subject: %v", jwtToken.Valid)
-	return "", fmt.Errorf("token not valid")
-
-}
diff --git a/route.go b/route.go
deleted file mode 100644
index 18d53e0..0000000
--- a/route.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package main
-
-import (
-	"net/http"
-)
-
-func buildRoute(r *http.ServeMux) {
-
-	r.HandleFunc("/v1/token", routeToken)
-	r.HandleFunc("/v1/boards", routeBoards)
-	r.HandleFunc("/v1/boards/", routeBoards)
-	r.HandleFunc("/v1/classes/", routeClasses)
-	r.HandleFunc("/v1/users/", routeUsers)
-}
diff --git a/route_boards.go b/route_boards.go
deleted file mode 100644
index 8bee1c6..0000000
--- a/route_boards.go
+++ /dev/null
@@ -1,197 +0,0 @@
-package main
-
-import (
-	"github.com/PichuChen/go-bbs"
-
-	"encoding/json"
-	"fmt"
-	"net/http"
-	"strings"
-)
-
-// routeBoards is the handler for `/v1/boards`
-func routeBoards(w http.ResponseWriter, r *http.Request) {
-	logger.Debugf("routeBoards: %v", r)
-
-	// TODO: Check IP Flowspeed
-	if r.Method == "GET" {
-		getBoards(w, r)
-		return
-	}
-
-}
-
-// getBoards is the handler for `/v1/boards` with GET method
-func getBoards(w http.ResponseWriter, r *http.Request) {
-	logger.Debugf("getBoards: %v", r)
-	boardId, item, filename, err := parseBoardPath(r.URL.Path)
-	if boardId == "" {
-		getBoardList(w, r)
-		return
-	}
-	// get single board
-	if item == "information" {
-		getBoardInformation(w, r, boardId)
-		return
-	} else if item == "articles" {
-		if filename == "" {
-			getBoardArticles(w, r, boardId)
-		} else {
-			getBoardArticlesFile(w, r, boardId, filename)
-		}
-		return
-	} else if item == "treasures" {
-		getBoardTreasures(w, r, boardId)
-		return
-	}
-
-	// 404
-	w.WriteHeader(http.StatusNotFound)
-
-	logger.Noticef("board id: %v not exist but be queried, info: %v err: %v", boardId, item, err)
-}
-
-func getBoardList(w http.ResponseWriter, r *http.Request) {
-	logger.Debugf("getBoardList: %v", r)
-
-	token := getTokenFromRequest(r)
-	userId, err := getUserIdFromToken(token)
-	if err != nil {
-		// user permission error
-		// Support Guest?
-		if !supportGuest() {
-			w.WriteHeader(http.StatusUnauthorized)
-			w.Write([]byte(`{"error":"token_invalid"}`))
-			return
-		} else {
-			userId = "guest" // TODO: use const variable
-		}
-	}
-
-	dataList := []interface{}{}
-	for _, b := range boardHeader {
-		// TODO: Show Board by user level
-		if b.IsClass() {
-			continue
-		}
-		if !shouldShowOnUserLevel(b, userId) {
-			continue
-		}
-		jb, _ := json.Marshal(b)
-		logger.Debugf("marshal board: %v", string(jb))
-		dataList = append(dataList, marshalBoardHeader(b))
-	}
-
-	responseMap := map[string]interface{}{
-		"data": dataList,
-	}
-
-	b, _ := json.MarshalIndent(responseMap, "", "  ")
-	w.Write(b)
-
-}
-
-func getBoardInformation(w http.ResponseWriter, r *http.Request, boardId string) {
-	logger.Debugf("getBoardInformation: %v", r)
-	token := getTokenFromRequest(r)
-	err := checkTokenPermission(token,
-		[]permission{PermissionReadBoardInformation},
-		map[string]string{
-			"board_id": boardId,
-		})
-
-	if err != nil {
-		// TODO: record unauthorized access
-		w.WriteHeader(http.StatusUnauthorized)
-		return
-	}
-
-	brd, err := findBoardHeaderById(boardId)
-	if err != nil {
-		// TODO: record error
-		logger.Warningf("find board %s failed: %v", boardId, err)
-		w.WriteHeader(http.StatusInternalServerError)
-		m := map[string]string{
-			"error":             "find_board_error",
-			"error_description": "get board for " + boardId + " failed",
-		}
-		b, _ := json.MarshalIndent(m, "", "  ")
-		w.Write(b)
-		return
-	}
-
-	responseMap := map[string]interface{}{
-		"data": marshalBoardHeader(brd),
-	}
-
-	b, _ := json.MarshalIndent(responseMap, "", "  ")
-	w.Write(b)
-
-}
-
-// marshal generate board or class metadata object,
-// b is input header
-func marshalBoardHeader(b bbs.BoardRecord) map[string]interface{} {
-	ret := map[string]interface{}{
-		"title":          b.Title(),
-		"number_of_user": "0",
-		"moderators":     b.BM(),
-	}
-	if b.IsClass() {
-		// class
-		// Assign ID from foreach loop
-		ret["type"] = "class"
-	} else {
-		// board
-		ret["id"] = b.BoardId()
-		ret["type"] = "board"
-	}
-	return ret
-
-}
-
-func shouldShowOnUserLevel(b bbs.BoardRecord, u string) bool {
-	// TODO: Get user Level
-	return true
-
-}
-
-// parseBoardPath covert url path from /v1/boards/SYSOP/article to
-// {SYSOP, article) or /v1/boards to {,}
-func parseBoardPath(path string) (boardId string, item string, filename string, err error) {
-	pathSegment := strings.Split(path, "/")
-
-	if len(pathSegment) >= 6 {
-		// /{{version}}/boards/{{class_id}}/{{item}}/{{filename}}
-		boardId = pathSegment[3]
-		item = pathSegment[4]
-		filename = pathSegment[5]
-		return
-	} else if len(pathSegment) == 5 {
-		// /{{version}}/boards/{{class_id}}/{{item}}
-		boardId = pathSegment[3]
-		item = pathSegment[4]
-		return
-	} else if len(pathSegment) == 4 {
-		// /{{version}}/boards/{{class_id}}
-		boardId = pathSegment[3]
-		return
-	} else if len(pathSegment) == 3 {
-		// /{{version}}/boards
-		// Should not be reach...
-		return
-	}
-	logger.Warningf("parseBoardPath got malform path: %v", path)
-	return
-
-}
-
-func findBoardHeaderById(boardId string) (bbs.BoardRecord, error) {
-	for _, it := range boardHeader {
-		if boardId == it.BoardId() {
-			return it, nil
-		}
-	}
-	return nil, fmt.Errorf("board record not found")
-
-}
diff --git a/route_boards_treasures.go b/route_boards_treasures.go
deleted file mode 100644
index b36f249..0000000
--- a/route_boards_treasures.go
+++ /dev/null
@@ -1,112 +0,0 @@
-package main
-
-import (
-	"github.com/PichuChen/go-bbs"
-
-	// "encoding/base64"
-	"encoding/json"
-	"fmt"
-	"net/http"
-	"strings"
-	// "os"
-)
-
-func getBoardTreasures(w http.ResponseWriter, r *http.Request, boardId string) {
-	logger.Debugf("getBoardTreasures: %v", r)
-	token := getTokenFromRequest(r)
-	_, treasuresId, filename, err := parseBoardTreasurePath(r.URL.Path)
-	if err != nil {
-		logger.Warningf("parseBoardTreasurePath error: %v", err)
-		// TODO return 400?
-	}
-	if filename != "" {
-		// get file
-		getBoardTreasuresFile(w, r, boardId, treasuresId, filename)
-		return
-	}
-
-	// Check permission for board
-	err = checkTokenPermission(token,
-		[]permission{PermissionReadTreasureInformation},
-		map[string]string{
-			"board_id":    boardId,
-			"treasure_id": strings.Join(treasuresId, ","),
-		})
-	if err != nil {
-		// TODO: record unauthorized access
-		w.WriteHeader(http.StatusUnauthorized)
-		return
-	}
-
-	var fileHeaders []bbs.ArticleRecord
-	fileHeaders, err = db.ReadBoardTreasureRecordsFile(boardId, treasuresId)
-	if err != nil {
-		logger.Warningf("open directory file error: %v", err)
-		// The board may not contain any article
-	}
-
-	items := []interface{}{}
-	for _, f := range fileHeaders {
-		m := map[string]interface{}{
-			"filename": f.Filename(),
-			// Bug(pichu): f.Modified time will be 0 when file is vote
-			"modified_time":   f.Modified(),
-			"recommend_count": f.Recommend(),
-			"post_date":       f.Date(),
-			"title":           f.Title(),
-			"money":           fmt.Sprintf("%v", f.Money()),
-			"owner":           f.Owner(),
-			// "aid": ""
-			"url": getArticleURL(boardId, f.Filename()),
-		}
-		items = append(items, m)
-	}
-	logger.Debugf("fh: %v", fileHeaders)
-
-	responseMap := map[string]interface{}{
-		"data": map[string]interface{}{
-			"items": items,
-		},
-	}
-
-	b, _ := json.MarshalIndent(responseMap, "", "  ")
-	w.Write(b)
-
-}
-
-func getBoardTreasuresFile(w http.ResponseWriter, r *http.Request, boardId string, treasuresId []string, filename string) {
-	logger.Debugf("getBoardTreasuresFile %v board: %v, treasuresId: %v, filename: %v", r, boardId, treasuresId, filename)
-
-	w.WriteHeader(http.StatusNotImplemented)
-}
-
-// parseBoardTreasurePath parse covert url path from /v1/boards/SYSOP/article to
-// {SYSOP, article) or /v1/boards to {,}
-func parseBoardTreasurePath(path string) (boardId string, treasuresId []string, filename string, err error) {
-	pathSegment := strings.Split(path, "/")
-
-	if len(pathSegment) == 6 {
-		// /{{version}}/boards/{{board_id}}/treasures/articles
-		boardId = pathSegment[3]
-		treasuresId = []string{}
-		filename = ""
-		return
-	} else if len(pathSegment) >= 7 {
-		// /{{version}}/boards/{{board_id}}/treasures/{{treasures_id ... }}/articles
-		// or
-		// /{{version}}/boards/{{board_id}}/treasures/{{treasures_id ... }}/articles/{{filename}}
-		boardId = pathSegment[3]
-		if pathSegment[len(pathSegment)-1] == "articles" {
-			treasuresId = pathSegment[5 : len(pathSegment)-1]
-			filename = ""
-		} else {
-			treasuresId = pathSegment[5 : len(pathSegment)-2]
-			filename = pathSegment[len(pathSegment)-1]
-		}
-		return
-	}
-	// should not be reached
-	logger.Warningf("parseBoardTreasurePath got malform path: %v", path)
-	return
-
-}
diff --git a/route_borads_articles.go b/route_borads_articles.go
deleted file mode 100644
index 35668f7..0000000
--- a/route_borads_articles.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package main
-
-import (
-	"github.com/PichuChen/go-bbs"
-
-	"encoding/base64"
-	"encoding/json"
-	"fmt"
-	"net/http"
-)
-
-// getBoardArticles handles request with `/v1/boards/SYSOP/articles` and will return
-// article list to client
-func getBoardArticles(w http.ResponseWriter, r *http.Request, boardId string) {
-	logger.Debugf("getBoardArticles: %v", r)
-	token := getTokenFromRequest(r)
-	// Check permission for board
-	err := checkTokenPermission(token,
-		[]permission{PermissionReadBoardInformation},
-		map[string]string{
-			"board_id": boardId,
-		})
-
-	if err != nil {
-		// TODO: record unauthorized access
-		w.WriteHeader(http.StatusUnauthorized)
-		return
-	}
-
-	var fileHeaders []bbs.ArticleRecord
-	fileHeaders, err = db.ReadBoardArticleRecordsFile(boardId)
-	if err != nil {
-		logger.Warningf("open directory file error: %v", err)
-		// The board may not contain any article
-	}
-
-	items := []interface{}{}
-	for _, f := range fileHeaders {
-		m := map[string]interface{}{
-			"filename": f.Filename(),
-			// Bug(pichu): f.Modified time will be 0 when file is vote
-			"modified_time":   f.Modified(),
-			"recommend_count": f.Recommend(),
-			"post_date":       f.Date(),
-			"title":           f.Title(),
-			"money":           fmt.Sprintf("%v", f.Money()),
-			"owner":           f.Owner(),
-			// "aid": ""
-			"url": getArticleURL(boardId, f.Filename()),
-		}
-		items = append(items, m)
-	}
-	logger.Debugf("fh: %v", fileHeaders)
-
-	responseMap := map[string]interface{}{
-		"data": map[string]interface{}{
-			"items": items,
-		},
-	}
-
-	b, _ := json.MarshalIndent(responseMap, "", "  ")
-	w.Write(b)
-
-}
-
-func getBoardArticlesFile(w http.ResponseWriter, r *http.Request, boardId string, filename string) {
-	logger.Debugf("getBoardArticlesFile: %v", r)
-
-	token := getTokenFromRequest(r)
-	err := checkTokenPermission(token,
-		[]permission{PermissionReadBoardInformation},
-		map[string]string{
-			"board_id": boardId,
-		})
-
-	if err != nil {
-		// TODO: record unauthorized access
-		w.WriteHeader(http.StatusUnauthorized)
-		return
-	}
-
-	buf, err := db.ReadBoardArticleFile(boardId, filename)
-	if err != nil {
-		logger.Errorf("read file %v/%v error: %v", boardId, filename, err)
-	}
-
-	bufStr := base64.StdEncoding.EncodeToString(buf)
-
-	responseMap := map[string]interface{}{
-		"data": map[string]interface{}{
-			"raw": bufStr,
-		},
-	}
-
-	b, _ := json.MarshalIndent(responseMap, "", "  ")
-	w.Write(b)
-}
-
-func getArticleURL(boardId string, filename string) string {
-	return fmt.Sprintf("https://ptt-app-dev-codingman.pichuchen.tw/bbs/%s/%s.html", boardId, filename)
-}
diff --git a/route_classes.go b/route_classes.go
deleted file mode 100644
index f9a2742..0000000
--- a/route_classes.go
+++ /dev/null
@@ -1,116 +0,0 @@
-package main
-
-import (
-	// "github.com/PichuChen/go-bbs"
-	// "github.com/PichuChen/go-bbs/crypt"
-	// "log"
-	"encoding/json"
-	"fmt"
-	"net/http"
-	"strings"
-)
-
-// routeClasses is the handler for `/v1/classes`
-func routeClasses(w http.ResponseWriter, r *http.Request) {
-	// TODO: Check IP Flowspeed
-
-	if r.Method == "GET" {
-		getClasses(w, r)
-		return
-	}
-
-}
-
-// getClasses HandleFunc handles path start with `/v1/classes`
-// and pass requests to next handle function
-func getClasses(w http.ResponseWriter, r *http.Request) {
-	logger.Debugf("getClasses: %v", r)
-	classId, item, err := parseClassPath(r.URL.Path)
-	logger.Noticef("query class: %v item: %v err: %v", classId, item, err)
-	if classId == "" {
-		getClassesWithoutClassId(w, r)
-		return
-	}
-	getClassesList(w, r, classId)
-	return
-
-	// // get single board
-	// if item == "information" {
-	// 	getBoardInformation(w, r, boardId)
-	// 	return
-	// }
-
-}
-
-// getClassesWithoutClassId handles path don't contain item after class id
-// eg: `/v1/classes`, it will redirect Client to `/v1/classes/1` which is
-// root class by default.
-func getClassesWithoutClassId(w http.ResponseWriter, r *http.Request) {
-	http.Redirect(w, r, "/v1/classes/1", 301)
-}
-
-// getClassesList handle path with class id and will return boards and classes
-// under this class.
-// TODO: What should we return when target class not found?
-func getClassesList(w http.ResponseWriter, r *http.Request, classId string) {
-	logger.Debugf("getClassesList: %v", r)
-
-	token := getTokenFromRequest(r)
-	userId, err := getUserIdFromToken(token)
-	if err != nil {
-		// user permission error
-		// Support Guest?
-		if !supportGuest() {
-			w.WriteHeader(http.StatusUnauthorized)
-			w.Write([]byte(`{"error":"token_invalid"}`))
-			return
-		} else {
-			userId = "guest" // TODO: use const variable
-		}
-	}
-
-	dataList := []interface{}{}
-	for bid, b := range boardHeader {
-		// TODO: Show Board by user level
-		if !shouldShowOnUserLevel(b, userId) {
-			continue
-		}
-		if b.ClassId() != classId {
-			continue
-		}
-		jb, _ := json.Marshal(b)
-		logger.Debugf("marshal class board: %v", string(jb))
-		m := marshalBoardHeader(b)
-		if b.IsClass() {
-			m["id"] = fmt.Sprintf("%v", bid+1)
-		}
-		dataList = append(dataList, m)
-	}
-
-	responseMap := map[string]interface{}{
-		"data": dataList,
-	}
-
-	b, _ := json.MarshalIndent(responseMap, "", "  ")
-	w.Write(b)
-
-}
-
-// parseClassPath covert url path from /v1/classes/1/information to
-// {1, information) or /v1/classes to {,}
-func parseClassPath(path string) (classId string, item string, err error) {
-	pathSegment := strings.Split(path, "/")
-	if len(pathSegment) == 5 {
-		// /{{version}}/classes/{{class_id}}/{{item}}
-		return pathSegment[3], pathSegment[4], nil
-	} else if len(pathSegment) == 4 {
-		// /{{version}}/classes/{{class_id}}
-		return pathSegment[3], "", nil
-	} else if len(pathSegment) == 3 {
-		// /{{version}}/classes
-		return "", "", nil
-	}
-	logger.Warningf("parseClassPath got malform path: %v", path)
-	return "", "", nil
-
-}
diff --git a/route_users.go b/route_users.go
deleted file mode 100644
index 3b078a8..0000000
--- a/route_users.go
+++ /dev/null
@@ -1,163 +0,0 @@
-package main
-
-import (
-	"github.com/PichuChen/go-bbs"
-
-	"encoding/json"
-	"fmt"
-	"net/http"
-	"strings"
-	"time"
-)
-
-func routeUsers(w http.ResponseWriter, r *http.Request) {
-	// TODO: Check IP Flowspeed
-
-	if r.Method == "GET" {
-		getUsers(w, r)
-		return
-	}
-
-}
-
-func getUsers(w http.ResponseWriter, r *http.Request) {
-	userId, item, err := parseUserPath(r.URL.Path)
-
-	if item == "information" {
-		getUserInformation(w, r, userId)
-		return
-	} else if item == "favorites" {
-		getUserFavorites(w, r, userId)
-		return
-	}
-	// else
-	logger.Noticef("user id: %v not exist but be queried, info: %v err: %v", userId, item, err)
-	w.WriteHeader(http.StatusNotFound)
-}
-
-func getUserInformation(w http.ResponseWriter, r *http.Request, userId string) {
-	token := getTokenFromRequest(r)
-	err := checkTokenPermission(token,
-		[]permission{PermissionReadUserInformation},
-		map[string]string{
-			"user_id": userId,
-		})
-
-	if err != nil {
-		// TODO: record unauthorized access
-		w.WriteHeader(http.StatusUnauthorized)
-		return
-	}
-
-	userrec, err := findUserecById(userId)
-	if err != nil {
-		// TODO: record error
-
-		w.WriteHeader(http.StatusInternalServerError)
-		m := map[string]string{
-			"error":             "find_userrec_error",
-			"error_description": "get userrec for " + userId + " failed",
-		}
-		b, _ := json.MarshalIndent(m, "", "  ")
-		w.Write(b)
-		return
-	}
-
-	// TODO: Check Etag or Not-Modified for cache
-
-	dataMap := map[string]interface{}{
-		"user_id":              userrec.UserId(),
-		"nickname":             userrec.Nickname(),
-		"realname":             userrec.RealName(),
-		"number_of_login_days": fmt.Sprintf("%d", userrec.NumLoginDays()),
-		"number_of_posts":      fmt.Sprintf("%d", userrec.NumPosts()),
-		// "number_of_badposts":   fmt.Sprintf("%d", userrec.NumLoginDays),
-		"money":           fmt.Sprintf("%d", userrec.Money()),
-		"last_login_time": userrec.LastLogin().Format(time.RFC3339),
-		"last_login_ipv4": userrec.LastHost(),
-		"last_login_ip":   userrec.LastHost(),
-		// "last_login_country": fmt.Sprintf("%d", userrec.NumLoginDays),
-		"chess_status": map[string]interface{}{},
-		"plan":         map[string]interface{}{},
-	}
-
-	responseMap := map[string]interface{}{
-		"data": dataMap,
-	}
-
-	responseByte, _ := json.MarshalIndent(responseMap, "", "  ")
-
-	w.Write(responseByte)
-}
-func getUserFavorites(w http.ResponseWriter, r *http.Request, userId string) {
-	token := getTokenFromRequest(r)
-	err := checkTokenPermission(token,
-		[]permission{PermissionReadUserInformation},
-		map[string]string{
-			"user_id": userId,
-		})
-
-	if err != nil {
-		// TODO: record unauthorized access
-		w.WriteHeader(http.StatusUnauthorized)
-		return
-	}
-
-	recs, err := db.ReadUserFavoriteRecords(userId)
-	logger.Debugf("file items length: %v", len(recs))
-	// dataMap := map[string]interface{}{}
-
-	dataItems := parseFavoriteFolderItem(recs)
-
-	responseMap := map[string]interface{}{
-		"data": map[string]interface{}{
-			"items": dataItems,
-		},
-	}
-
-	responseByte, _ := json.MarshalIndent(responseMap, "", "  ")
-
-	w.Write(responseByte)
-}
-
-func parseFavoriteFolderItem(recs []bbs.FavoriteRecord) []interface{} {
-	dataItems := []interface{}{}
-	for _, item := range recs {
-		logger.Debugf("fav type: %v", item.Type())
-
-		switch item.Type() {
-		case bbs.FavoriteTypeBoard:
-			dataItems = append(dataItems, map[string]interface{}{
-				"type":     "board",
-				"board_id": item.BoardId(),
-			})
-
-		case bbs.FavoriteTypeFolder:
-			dataItems = append(dataItems, map[string]interface{}{
-				"type":  "folder",
-				"title": item.Title(),
-				"items": parseFavoriteFolderItem(item.Records()),
-			})
-
-		case bbs.FavoriteTypeLine:
-			dataItems = append(dataItems, map[string]interface{}{
-				"type": "line",
-			})
-		default:
-			logger.Warningf("parseFavoriteFolderItem unknown favItem type")
-		}
-	}
-	return dataItems
-}
-
-func parseUserPath(path string) (userId string, item string, err error) {
-	pathSegment := strings.Split(path, "/")
-	// /{{version}}/users/{{user_id}}/{{item}}
-	if len(pathSegment) == 4 {
-		// /{{version}}/users/{{user_id}}
-		return pathSegment[3], "", nil
-	}
-
-	return pathSegment[3], pathSegment[4], nil
-
-}


Test patch:
diff --git a/route_users_test.go b/internal/delivery/http/route_users_test.go
similarity index 91%
rename from route_users_test.go
rename to internal/delivery/http/route_users_test.go
index 6bdf087..49ae0a2 100644
--- a/route_users_test.go
+++ b/internal/delivery/http/route_users_test.go
@@ -1,7 +1,7 @@
-package main
+package http
 
 import (
-	"github.com/PichuChen/go-bbs"
+	"github.com/Ptt-official-app/Ptt-backend/internal/mock"
 
 	"encoding/json"
 	"net/http"
@@ -52,24 +52,21 @@ func (u *MockUserRecord) LastHost() string { return "" }
 
 func TestGetUserInformation(t *testing.T) {
 
-	expected := NewMockUserRecord("SYSOP")
-
-	userRecs = []bbs.UserRecord{
-		expected,
-	}
-
+	userID := "id"
+	usecase := mock.NewMockUsecase()
+	delivery := NewHTTPDelivery(usecase)
 	req, err := http.NewRequest("GET", "/v1/users/SYSOP/information", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	token := newAccessTokenWithUsername(expected.UserId())
+	token := usecase.CreateAccessTokenWithUsername(userID)
 	t.Logf("testing token: %v", token)
 	req.Header.Add("Authorization", "bearer "+token)
 
 	rr := httptest.NewRecorder()
 	r := http.NewServeMux()
-	r.HandleFunc("/v1/users/", routeUsers)
+	r.HandleFunc("/v1/users/", delivery.routeUsers)
 	r.ServeHTTP(rr, req)
 
 	if status := rr.Code; status != http.StatusOK {
@@ -81,12 +78,10 @@ func TestGetUserInformation(t *testing.T) {
 	json.Unmarshal(rr.Body.Bytes(), &responsedMap)
 	t.Logf("got response %v", rr.Body.String())
 	responsedData := responsedMap["data"].(map[string]interface{})
-	if responsedData["user_id"] != expected.UserId() {
+	if responsedData["user_id"] != userID {
 		t.Errorf("handler returned unexpected body, user_id not match: got %v want userId %v",
-			rr.Body.String(), expected.UserId())
-
+			rr.Body.String(), userID)
 	}
-
 }
 
 func TestParseUserPath(t *testing.T) {