diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..2c2a54a
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,29 @@
+name: 'test'
+
+on:
+  push:
+    branches:
+      - 'main'
+  pull_request:
+
+jobs:
+  tests:
+    runs-on: ubuntu-latest
+    
+    strategy:
+      matrix:
+        lisp:
+          - sbcl-bin
+          - ccl-bin
+          
+    env:
+      LISP: ${{ matrix.lisp }}
+
+    steps:
+      - uses: actions/checkout@v4
+      - uses: 40ants/setup-lisp@v4
+        with:
+          asdf-system: ningle-fbr
+      - uses: 40ants/run-tests@v2
+        with:
+          asdf-system: ningle-fbr
diff --git a/README.md b/README.md
index ee74370..0b2d284 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,37 @@
-# ningle-fbr (WIP)
+# ningle-fbr
 
-A utility for [ningle](https://github.com/fukamachi/ningle) and [jingle](https://github.com/dnaeon/cl-jingle) to enable file-based routing.
+A file-based router for [ningle](https://github.com/fukamachi/ningle).
+
+## Warning
+
+This software is currently in ALPHA stage. Its APIs are subject to change.  
+
+Check the [release notes](https://github.com/skyizwhite/ningle-fbr/releases) for the latest updates.
 
 ## What is File-Based Routing?
 
-File-based routing is a concept commonly used in modern web frameworks such as [Next.js](https://nextjs.org/). Instead of explicitly defining routes through configuration or code, the framework automatically sets up routes based on the file hierarchy of a specific directory (usually the "pages" or "routes" directory).
+File-based routing automatically creates URL routes based on a project’s file and directory structure. Instead of manually configuring routes in a separate routing file, each file in a designated directory (e.g., `pages` or `routes`) becomes a route. This simplifies development and maintenance since adding, removing, or renaming a route is often just a matter of modifying a file’s name or location.
 
 ## Usage
 
-To use ningle-fbr, you need to use [package-inferred-system](https://asdf.common-lisp.dev/asdf/The-package_002dinferred_002dsystem-extension.html).
+To use ningle-fbr, set up your project using the [package-inferred-system](https://asdf.common-lisp.dev/asdf/The-package_002dinferred_002dsystem-extension.html) style.
 
-`/example.asd`
+**Example directory structure**:
+```
+example.asd
+src/
+  app.lisp
+  routes/
+    index.lisp
+    hello.lisp
+    users/
+      index.lisp
+      <id>.lisp
+    nested/
+      page.lisp
+```
+
+**`example.asd`**:
 ```lisp
 (defsystem "example"
   :class :package-inferred-system
@@ -18,33 +39,30 @@ To use ningle-fbr, you need to use [package-inferred-system](https://asdf.common
   :depends-on ("example/app"))
 ```
 
-`/src/app.lisp`
+**`/src/app.lisp`**:
 ```lisp
 (defpackage #:example
   (:nicknames #:example/app)
   (:use #:cl)
   (:import-from #:ningle)
-  (:import-from #:ningle-fbr
-                #:assign-routes))
+  (:import-from #:ningle-fbr #:set-routes))
 (in-package #:example/app)
 
 (defparameter *app* (make-instance 'ningle:<app>))
 
-(assign-routes *app*
-               :directory "src/routes"
-               :system "example")
+(set-routes *app* :system :example :target-dir-path "routes")
 ```
 
 ### Static Routing
 
-`/src/routes/index.lisp` → `/`
+Routes are determined by packages located under `:example/routes`. The package’s name corresponds directly to a URL path:
 
-`/src/routes/hello.lisp` → `/hello`
-
-`/src/routes/users/index.lisp` → `/users`
-
-`/src/routes/nested/page.lisp` → `/nested/page`
+- `:example/routes/index` → `/`
+- `:example/routes/hello` → `/hello`
+- `:example/routes/users/index` → `/users`
+- `:example/routes/nested/page` → `/nested/page`
 
+**`/src/routes/index.lisp`**:
 ```lisp
 (defpackage #:example/routes/index
   (:use #:cl)
@@ -55,42 +73,50 @@ To use ningle-fbr, you need to use [package-inferred-system](https://asdf.common
 (in-package #:example/routes/index)
 
 (defun handle-get (params)
-  ...)
+  ;; Implement GET logic here
+  )
 
 (defun handle-post (params)
-  ...)
+  ;; Implement POST logic here
+  )
 
 (defun handle-put (params)
-  ...)
+  ;; Implement PUT logic here
+  )
 
 (defun handle-delete (params)
-  ...)
+  ;; Implement DELETE logic here
+  )
 ```
 
+Handlers are chosen based on the HTTP method. If `handle-get` is exported, it will be called for `GET` requests on `/`.
+
 ### Dynamic Routing
 
-A file or directory name prefixed with '=' indicates a dynamic path.
+To define dynamic routes, use `<>` in the file name to indicate URL parameters.
 
-In the example below, the parameter `id` can be obtained from the handler's params.
+For example:
+`:example/routes/user/<id>` → `/user/:id`
 
-`/src/routes/user/=id.lisp` → `/user/:id`
+If a request comes in at `/user/123`, `params` will include `:id "123"`.
 
-### Not Found Error
+### 404 Handling
 
-`not-found.lisp` is a special file to handle 404 errors. Implement the `handle-not-found` function and export it.
+To handle 404 (Not Found) errors, create a special package named `:example/routes/not-found` and define `handle-not-found`:
 
 ```lisp
 (defpackage #:example/routes/not-found
   (:use #:cl)
   (:export #:handle-not-found))
 (in-package #:example/routes/not-found)
-  
+
 (defun handle-not-found ()
-  ...)
+  ;; Implement custom 404 logic here
+  )
 ```
 
 ## License
 
 Licensed under the MIT License.
 
-© 2024, skyizwhite.
+© 2024, skyizwhite.
\ No newline at end of file
diff --git a/ningle-fbr-test.asd b/ningle-fbr-test.asd
new file mode 100644
index 0000000..47b744e
--- /dev/null
+++ b/ningle-fbr-test.asd
@@ -0,0 +1,6 @@
+(defsystem "ningle-fbr-test"
+  :class :package-inferred-system
+  :pathname "tests"
+  :depends-on ("rove"
+               "ningle-fbr-test/router")
+  :perform (test-op (o c) (symbol-call :rove :run c :style :dot)))
diff --git a/ningle-fbr-tests.asd b/ningle-fbr-tests.asd
deleted file mode 100644
index 8bdb5a1..0000000
--- a/ningle-fbr-tests.asd
+++ /dev/null
@@ -1,9 +0,0 @@
-(defsystem "ningle-fbr-tests"
-  :author "skyizwhite <paku@skyizwhite.dev>"
-  :license "MIT"
-  :class :package-inferred-system
-  :pathname "tests"
-  :depends-on ("rove"
-               "ningle-fbr-tests/main")
-  :perform (test-op (o c)
-                    (symbol-call :rove '#:run c)))
diff --git a/ningle-fbr.asd b/ningle-fbr.asd
index 9b2737e..a567398 100644
--- a/ningle-fbr.asd
+++ b/ningle-fbr.asd
@@ -1,11 +1,12 @@
 (defsystem "ningle-fbr"
   :version "0.1.0"
-  :description "Plugin for ningle to enable file-based routing"
+  :description "File-based router for Ningle"
   :long-description #.(uiop:read-file-string
                        (uiop:subpathname *load-pathname* "README.md"))
-  :author "skyizwhite <paku@skyizwhite.dev>"
+  :author "skyizwhite"
+  :maintainer "skyizwhite <paku@skyizwhite.dev>"
   :license "MIT"
   :class :package-inferred-system
   :pathname "src"
   :depends-on ("ningle-fbr/main")
-  :in-order-to ((test-op (test-op "ningle-fbr-tests"))))
+  :in-order-to ((test-op (test-op "ningle-fbr-test"))))
diff --git a/qlfile b/qlfile
index 087ffa1..88e3eca 100644
--- a/qlfile
+++ b/qlfile
@@ -1,3 +1,7 @@
 ql ningle
+ql lack
 ql cl-ppcre
-ql rove
+ql alexandria
+ql trivial-system-loader
+github rove fukamachi/rove
+github dissect shinmera/dissect
diff --git a/qlfile.lock b/qlfile.lock
index 5634b14..251711d 100644
--- a/qlfile.lock
+++ b/qlfile.lock
@@ -6,11 +6,27 @@
  (:class qlot/source/ql:source-ql
   :initargs (:%version :latest)
   :version "ql-2023-10-21"))
+("lack" .
+ (:class qlot/source/ql:source-ql
+  :initargs (:%version :latest)
+  :version "ql-2024-10-12"))
 ("cl-ppcre" .
  (:class qlot/source/ql:source-ql
   :initargs (:%version :latest)
   :version "ql-2023-10-21"))
-("rove" .
+("alexandria" .
  (:class qlot/source/ql:source-ql
   :initargs (:%version :latest)
-  :version "ql-2023-10-21"))
+  :version "ql-2024-10-12"))
+("trivial-system-loader" .
+ (:class qlot/source/ql:source-ql
+  :initargs (:%version :latest)
+  :version "ql-2024-10-12"))
+("rove" .
+ (:class qlot/source/github:source-github
+  :initargs (:repos "fukamachi/rove" :ref nil :branch nil :tag nil)
+  :version "github-cacea7331c10fe9d8398d104b2dfd579bf7ea353"))
+("dissect" .
+ (:class qlot/source/github:source-github
+  :initargs (:repos "shinmera/dissect" :ref nil :branch nil :tag nil)
+  :version "github-a70cabcd748cf7c041196efd711e2dcca2bbbb2c"))
diff --git a/src/main.lisp b/src/main.lisp
index 0c3632a..8236432 100644
--- a/src/main.lisp
+++ b/src/main.lisp
@@ -1,68 +1,6 @@
 (uiop:define-package :ningle-fbr
   (:nicknames #:ningle-fbr/main)
-  (:use #:cl)
-  (:import-from #:cl-ppcre)
-  (:import-from #:ningle)
-  (:export #:assign-routes))
+  (:use #:cl
+        #:ningle-fbr/router)
+  (:export #:set-routes))
 (in-package :ningle-fbr)
-
-(defun remove-file-type (namestr)
-  (cl-ppcre:regex-replace ".lisp" namestr ""))
-
-(defun remove-index (url)
-  (if (string= url "/index")
-      "/"
-      (cl-ppcre:regex-replace "/index" url "")))
-
-(defun replace-dynamic-annotation (url)
-  (cl-ppcre:regex-replace "=" url ":"))
-
-(defun format-url (url)
-  (replace-dynamic-annotation (remove-index url)))
-
-(defun pathname->url (pathname dir-namestring)
-  (format-url
-   (cl-ppcre:regex-replace dir-namestring
-                           (remove-file-type (namestring pathname))
-                           "")))
-
-(defun pathname->package (pathname system-path-namestring system-prefix)
-  (string-upcase
-   (cl-ppcre:regex-replace system-path-namestring
-                           (remove-file-type (namestring pathname))
-                           system-prefix)))
-
-(defun dir->pathnames (dir)
-  (directory (concatenate 'string
-                          dir
-                          "/**/*.lisp")))
-
-(defun dir->urls-and-packages (dir system)
-  (let ((dir-namestring (namestring
-                         (asdf:system-relative-pathname system dir)))
-        (system-path-namestring (namestring 
-                                 (asdf/component:component-relative-pathname
-                                  (asdf/find-system:find-system system))))
-        (system-prefix (concatenate 'string system "/")))
-    (mapcar (lambda (pathname)
-              (cons (pathname->url pathname dir-namestring)
-                    (pathname->package pathname system-path-namestring system-prefix)))
-            (dir->pathnames dir))))
-
-(defparameter *http-request-methods*
-  '(:GET :POST :PUT :DELETE :HEAD :CONNECT :OPTIONS :PATCH))
-
-(defun assign-routes (app &key directory system)
-  (loop
-    :for (url . pkg) :in (dir->urls-and-packages directory system)
-    :do (ql:quickload pkg)
-        (if (string= url "/not-found")
-            (let ((handler (find-symbol "HANDLE-NOT-FOUND" pkg)))
-              (defmethod ningle:not-found ((app ningle:app))
-                (funcall handler))))
-        (loop
-          :for method :in *http-request-methods*
-          :do (let ((handler (find-symbol (concatenate 'string "HANDLE-" (string method))
-                                          pkg)))
-                (when handler
-                  (setf (ningle:route app url :method method) handler))))))
diff --git a/src/router.lisp b/src/router.lisp
new file mode 100644
index 0000000..de26076
--- /dev/null
+++ b/src/router.lisp
@@ -0,0 +1,69 @@
+(defpackage #:ningle-fbr/router
+  (:use #:cl)
+  (:import-from #:alexandria
+                #:make-keyword)
+  (:import-from #:cl-ppcre
+                #:quote-meta-chars
+                #:regex-replace
+                #:regex-replace-all)
+  (:import-from #:ningle)
+  (:import-from #:trivial-system-loader
+                #:load-system)
+  (:export #:pathname->path
+           #:path->uri
+           #:path-package
+           #:set-routes))
+(in-package #:ningle-fbr/router)
+
+(defun pathname->path (pathname target-dir-pathname)
+  (let* ((full (namestring pathname))
+         (prefix (quote-meta-chars (namestring target-dir-pathname))))
+    (regex-replace (format nil "^~A(.*?).lisp$" prefix) full "/\\1")))
+
+(defun detect-paths (system target-dir-path)
+  (let ((target-dir-pathname
+          (merge-pathnames (concatenate 'string
+                                        target-dir-path
+                                        "/")
+                           (asdf:component-pathname (asdf:find-system system)))))
+    (mapcar (lambda (pathname)
+              (pathname->path pathname target-dir-pathname))
+            (directory (merge-pathnames "**/*.lisp" target-dir-pathname)))))
+
+(defun remove-index (path)
+  (if (string= path "/index")
+      "/"
+      (regex-replace "/index$" path "")))
+
+(defun bracket->colon (path)
+  (regex-replace-all "<(.*?)>" path ":\\1"))
+
+(defun path->uri (path)
+  (bracket->colon (remove-index path)))
+
+(defun path->package (path system target-dir-path)
+  (make-keyword (string-upcase (concatenate 'string
+                                            (string system)
+                                            "/"
+                                            target-dir-path
+                                            path))))
+
+(defparameter *http-request-methods*
+  '(:GET :POST :PUT :DELETE :HEAD :CONNECT :OPTIONS :PATCH))
+
+(defmethod set-routes ((app ningle:app) &key system target-dir-path)
+  (loop
+    :for path :in (detect-paths system target-dir-path)
+    :for uri := (path->uri path)
+    :for pkg := (path->package path system target-dir-path)
+    :do (load-system pkg)
+        (if (string= uri "/not-found")
+            (let ((handler (find-symbol "HANDLE-NOT-FOUND" pkg)))
+              (defmethod ningle:not-found ((app ningle:app))
+                (funcall handler))))
+        (loop
+          :for method :in *http-request-methods*
+          :do (let ((handler (find-symbol (concatenate 'string "HANDLE-" (string method))
+                                          pkg)))
+                (when handler
+                  (setf (ningle:route app uri :method method) handler))))))
diff --git a/tests/main.lisp b/tests/main.lisp
deleted file mode 100644
index 06f02eb..0000000
--- a/tests/main.lisp
+++ /dev/null
@@ -1,4 +0,0 @@
-(defpackage #:ningle-fbr-tests/main
-  (:use #:cl
-        #:rove))
-(in-package #:ningle-fbr-tests/main)
diff --git a/tests/router.lisp b/tests/router.lisp
new file mode 100644
index 0000000..03724a0
--- /dev/null
+++ b/tests/router.lisp
@@ -0,0 +1,83 @@
+(defpackage #:ningle-fbr-test/router
+  (:use #:cl
+        #:rove)
+  (:import-from #:ningle)
+  (:import-from #:lack)
+  (:import-from #:lack/test
+                #:testing-app
+                #:request)
+  (:import-from #:ningle-fbr/router
+                #:path->uri
+                #:path->package
+                #:pathname->path
+                #:set-routes))
+(in-package #:ningle-fbr-test/router)
+
+(deftest uri-test
+  (testing "normal path"
+    (ok (string= (path->uri "/foo") "/foo"))
+    (ok (string= (path->uri "/foo/bar") "/foo/bar")))
+  
+  (testing "index path"
+    (ok (string= (path->uri "/index") "/"))
+    (ok (string= (path->uri "/nested/index") "/nested")))
+  
+  (testing "dynamic path"
+    (ok (string= (path->uri "/user/<id>") "/user/:id"))
+    (ok (string= (path->uri "/location/<country>/<city>") "/location/:country/:city"))))
+
+(deftest package-test
+  (testing "normal case"
+    (ok (eq (path->package "/foo" :app "routes")
+            :app/routes/foo))
+    (ok (eq (path->package "/foo" :app "somedir/routes")
+            :app/somedir/routes/foo))))
+
+(deftest router-test
+  (testing "pathname->path"
+    (ok (string= (pathname->path #P"/home/app/src/routes/foo.lisp"
+                                 #P"/home/app/src/routes/")
+                 "/foo")))
+  
+  (testing "set-routes"
+    (testing-app (let ((app (make-instance 'ningle:app)))
+                   (set-routes app
+                               :system :ningle-fbr-test
+                               :target-dir-path "routes")
+                   (lack:builder app))
+      (multiple-value-bind (body status headers)
+          (request "/")
+        (declare (ignore headers))
+        (ok (string= body "ok"))
+        (ok (eql status 200)))
+
+      (multiple-value-bind (body status headers)
+          (request "/hello")
+        (declare (ignore headers))
+        (ok (string= body "ok"))
+        (ok (eql status 200)))
+      
+      (multiple-value-bind (body status headers)
+          (request "/nested/page")
+        (declare (ignore headers))
+        (ok (string= body "ok"))
+        (ok (eql status 200)))
+      
+      (multiple-value-bind (body status headers)
+          (request "/users")
+        (declare (ignore headers))
+        (ok (string= body "ok"))
+        (ok (eql status 200)))
+      
+      (multiple-value-bind (body status headers)
+          (request "/users/bob")
+        (declare (ignore headers))
+        (ok (string= body "bob"))
+        (ok (eql status 200)))
+      
+      (multiple-value-bind (body status headers)
+          (request "/missing")
+        (declare (ignore headers))
+        (ok (string= body "Not Found"))
+        (ok (eql status 404))))))
+
diff --git a/tests/routes/hello.lisp b/tests/routes/hello.lisp
new file mode 100644
index 0000000..18beefa
--- /dev/null
+++ b/tests/routes/hello.lisp
@@ -0,0 +1,8 @@
+(defpackage #:ningle-fbr-test/routes/hello
+  (:use #:cl)
+  (:export #:handle-get))
+(in-package #:ningle-fbr-test/routes/hello)
+
+(defun handle-get (params)
+  (declare (ignore params))
+  "ok")
diff --git a/tests/routes/index.lisp b/tests/routes/index.lisp
new file mode 100644
index 0000000..dab5d79
--- /dev/null
+++ b/tests/routes/index.lisp
@@ -0,0 +1,8 @@
+(defpackage #:ningle-fbr-test/routes/index
+  (:use #:cl)
+  (:export #:handle-get))
+(in-package #:ningle-fbr-test/routes/index)
+
+(defun handle-get (params)
+  (declare (ignore params))
+  "ok")
diff --git a/tests/routes/nested/page.lisp b/tests/routes/nested/page.lisp
new file mode 100644
index 0000000..afc9f62
--- /dev/null
+++ b/tests/routes/nested/page.lisp
@@ -0,0 +1,8 @@
+(defpackage #:ningle-fbr-test/routes/nested/page
+  (:use #:cl)
+  (:export #:handle-get))
+(in-package #:ningle-fbr-test/routes/nested/page)
+
+(defun handle-get (params)
+  (declare (ignore params))
+  "ok")
diff --git a/tests/routes/not-found.lisp b/tests/routes/not-found.lisp
new file mode 100644
index 0000000..5392fd8
--- /dev/null
+++ b/tests/routes/not-found.lisp
@@ -0,0 +1,11 @@
+(defpackage #:ningle-fbr-test/routes/not-found
+  (:use #:cl)
+  (:import-from #:lack/response)
+  (:import-from #:ningle)
+  (:export #:handle-not-found))
+(in-package #:ningle-fbr-test/routes/not-found)
+
+(defun handle-not-found ()
+  (setf (lack/response:response-status ningle:*response*)
+        404)
+  "Not Found")
diff --git a/tests/routes/users/<id>.lisp b/tests/routes/users/<id>.lisp
new file mode 100644
index 0000000..0eb385f
--- /dev/null
+++ b/tests/routes/users/<id>.lisp
@@ -0,0 +1,8 @@
+(defpackage #:ningle-fbr-test/routes/users/<id>
+  (:use #:cl)
+  (:export #:handle-get))
+(in-package #:ningle-fbr-test/routes/users/<id>)
+
+(defun handle-get (params)
+  (let ((id (cdr (assoc :id params))))
+    id))
diff --git a/tests/routes/users/index.lisp b/tests/routes/users/index.lisp
new file mode 100644
index 0000000..5473c9b
--- /dev/null
+++ b/tests/routes/users/index.lisp
@@ -0,0 +1,8 @@
+(defpackage #:ningle-fbr-test/routes/users/index
+  (:use #:cl)
+  (:export #:handle-get))
+(in-package #:ningle-fbr-test/routes/users/index)
+
+(defun handle-get (params)
+  (declare (ignore params))
+  "ok")