diff --git a/interface-definitions/include/version/https-version.xml.i b/interface-definitions/include/version/https-version.xml.i
index 525314dbd..a889a7805 100644
--- a/interface-definitions/include/version/https-version.xml.i
+++ b/interface-definitions/include/version/https-version.xml.i
@@ -1,3 +1,3 @@
 <!-- include start from include/version/https-version.xml.i -->
-<syntaxVersion component='https' version='6'></syntaxVersion>
+<syntaxVersion component='https' version='7'></syntaxVersion>
 <!-- include end -->
diff --git a/interface-definitions/service_https.xml.in b/interface-definitions/service_https.xml.in
index afe430c0c..7bb63fa5a 100644
--- a/interface-definitions/service_https.xml.in
+++ b/interface-definitions/service_https.xml.in
@@ -1,190 +1,197 @@
 <?xml version="1.0"?>
 <interfaceDefinition>
   <node name="service">
     <children>
       <node name="https" owner="${vyos_conf_scripts_dir}/service_https.py">
         <properties>
           <help>HTTPS configuration</help>
           <priority>1001</priority>
         </properties>
         <children>
           <node name="api">
             <properties>
               <help>VyOS HTTP API configuration</help>
             </properties>
             <children>
               <node name="keys">
                 <properties>
                   <help>HTTP API keys</help>
                 </properties>
                 <children>
                   <tagNode name="id">
                     <properties>
                       <help>HTTP API id</help>
                     </properties>
                     <children>
                       <leafNode name="key">
                         <properties>
                           <help>HTTP API plaintext key</help>
                         </properties>
                       </leafNode>
                     </children>
                   </tagNode>
                 </children>
               </node>
-              <leafNode name="strict">
+              <node name="rest">
                 <properties>
-                  <help>Enforce strict path checking</help>
-                  <valueless/>
+                  <help>REST API</help>
                 </properties>
-              </leafNode>
-              <leafNode name="debug">
-                <properties>
-                  <help>Debug</help>
-                  <valueless/>
-                  <hidden/>
-                </properties>
-              </leafNode>
+                <children>
+                  <leafNode name="strict">
+                    <properties>
+                      <help>Enforce strict path checking</help>
+                      <valueless/>
+                    </properties>
+                  </leafNode>
+                  <leafNode name="debug">
+                    <properties>
+                      <help>Debug</help>
+                      <valueless/>
+                      <hidden/>
+                    </properties>
+                  </leafNode>
+                </children>
+              </node>
               <node name="graphql">
                 <properties>
-                  <help>GraphQL support</help>
+                  <help>GraphQL API</help>
                 </properties>
                 <children>
                   <leafNode name="introspection">
                     <properties>
                       <help>Schema introspection</help>
                       <valueless/>
                     </properties>
                   </leafNode>
                   <node name="authentication">
                     <properties>
                       <help>GraphQL authentication</help>
                     </properties>
                     <children>
                       <leafNode name="type">
                         <properties>
                           <help>Authentication type</help>
                           <completionHelp>
                             <list>key token</list>
                           </completionHelp>
                           <valueHelp>
                             <format>key</format>
                             <description>Use API keys</description>
                           </valueHelp>
                           <valueHelp>
                             <format>token</format>
                             <description>Use JWT token</description>
                           </valueHelp>
                           <constraint>
                             <regex>(key|token)</regex>
                           </constraint>
                         </properties>
                         <defaultValue>key</defaultValue>
                       </leafNode>
                       <leafNode name="expiration">
                         <properties>
                           <help>Token time to expire in seconds</help>
                           <valueHelp>
                             <format>u32:60-31536000</format>
                             <description>Token lifetime in seconds</description>
                           </valueHelp>
                           <constraint>
                             <validator name="numeric" argument="--range 60-31536000"/>
                           </constraint>
                         </properties>
                         <defaultValue>3600</defaultValue>
                       </leafNode>
                       <leafNode name="secret-length">
                         <properties>
                           <help>Length of shared secret in bytes</help>
                           <valueHelp>
                             <format>u32:16-65535</format>
                             <description>Byte length of generated shared secret</description>
                           </valueHelp>
                           <constraint>
                             <validator name="numeric" argument="--range 16-65535"/>
                           </constraint>
                         </properties>
                         <defaultValue>32</defaultValue>
                       </leafNode>
                     </children>
                   </node>
-                </children>
-              </node>
-              <node name="cors">
-                <properties>
-                  <help>Set CORS options</help>
-                </properties>
-                <children>
-                  <leafNode name="allow-origin">
+                  <node name="cors">
                     <properties>
-                      <help>Allow resource request from origin</help>
-                      <multi/>
+                      <help>Set CORS options</help>
                     </properties>
-                  </leafNode>
+                    <children>
+                      <leafNode name="allow-origin">
+                        <properties>
+                          <help>Allow resource request from origin</help>
+                          <multi/>
+                        </properties>
+                      </leafNode>
+                    </children>
+                  </node>
                 </children>
               </node>
             </children>
           </node>
           #include <include/allow-client.xml.i>
           <leafNode name="enable-http-redirect">
             <properties>
               <help>Enable HTTP to HTTPS redirect</help>
               <valueless/>
             </properties>
           </leafNode>
           #include <include/listen-address.xml.i>
           #include <include/port-number.xml.i>
           <leafNode name='port'>
             <defaultValue>443</defaultValue>
           </leafNode>
           <leafNode name="request-body-size-limit">
             <properties>
               <help>Maximum request body size in megabytes</help>
               <valueHelp>
                 <format>u32:1-256</format>
                 <description>Request body size in megabytes</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 1-256"/>
               </constraint>
             </properties>
             <defaultValue>1</defaultValue>
           </leafNode>
           <node name="certificates">
             <properties>
               <help>TLS certificates</help>
             </properties>
             <children>
               #include <include/pki/ca-certificate.xml.i>
               #include <include/pki/certificate.xml.i>
               #include <include/pki/dh-params.xml.i>
             </children>
           </node>
           <leafNode name="tls-version">
             <properties>
               <help>Specify available TLS version(s)</help>
               <completionHelp>
                 <list>1.2 1.3</list>
               </completionHelp>
               <valueHelp>
                 <format>1.2</format>
                 <description>TLSv1.2</description>
               </valueHelp>
               <valueHelp>
                 <format>1.3</format>
                 <description>TLSv1.3</description>
               </valueHelp>
               <constraint>
                 <regex>(1.2|1.3)</regex>
               </constraint>
               <multi/>
             </properties>
             <defaultValue>1.2 1.3</defaultValue>
           </leafNode>
           #include <include/interface/vrf.xml.i>
         </children>
       </node>
     </children>
   </node>
 </interfaceDefinition>
diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py
index 8a6386e4f..04c4a2e51 100755
--- a/smoketest/scripts/cli/test_service_https.py
+++ b/smoketest/scripts/cli/test_service_https.py
@@ -1,502 +1,551 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2019-2024 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import unittest
 import json
 
 from requests import request
 from urllib3.exceptions import InsecureRequestWarning
 from time import sleep
 
 from base_vyostest_shim import VyOSUnitTestSHIM
 from base_vyostest_shim import ignore_warning
 from vyos.utils.file import read_file
 from vyos.utils.file import write_file
 from vyos.utils.process import call
 from vyos.utils.process import process_named_running
 from vyos.xml_ref import default_value
 
 from vyos.configsession import ConfigSessionError
 
 base_path = ['service', 'https']
 pki_base = ['pki']
 
 cert_data = """
 MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIw
 WTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv
 bWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIx
 MDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNV
 BAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlP
 UzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
 01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3
 QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E
 BAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu
 +JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3Lftz
 ngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93
 +dm/LDnp7C0=
 """
 
 key_data = """
 MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx
 2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7
 u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww
 """
 
 dh_1024 = """
 MIGHAoGBAM3nvMkHGi/xmRs8cYg4pcl5sAanxel9EM+1XobVhUViXw8JvlmSEVOj
 n2aXUifc4SEs3WDzVPRC8O8qQWjvErpTq/HOgt3aqBCabMgvflmt706XP0KiqnpW
 EyvNiI27J3wBUzEXLIS110MxPAX5Tcug974PecFcOxn1RWrbWcx/AgEC
 """
 
 dh_2048 = """
 MIIBCAKCAQEA1mld/V7WnxxRinkOlhx/BoZkRELtIUQFYxyARBqYk4C5G3YnZNNu
 zjaGyPnfIKHu8SIUH85OecM+5/co9nYlcUJuph2tbR6qNgPw7LOKIhf27u7WhvJk
 iVsJhwZiWmvvMV4jTParNEI2svoooMyhHXzeweYsg6YtgLVmwiwKj3XP3gRH2i3B
 Mq8CDS7X6xaKvjfeMPZBFqOM5nb6HhsbaAUyiZxrfipLvXxtnbzd/eJUQVfVdxM3
 pn0i+QrO2tuNAzX7GoPc9pefrbb5xJmGS50G0uqsR59+7LhYmyZSBASA0lxTEW9t
 kv/0LPvaYTY57WL7hBeqqHy/WPZHPzDI3wIBAg==
 """
 # to test load config via HTTP URL
 nginx_tmp_site = '/etc/nginx/sites-enabled/smoketest'
 nginx_conf_smoketest = """
 server {
     listen 8000;
     server_name localhost;
 
     root /tmp;
 
     index index.html;
 
     location / {
         try_files $uri $uri/ =404;
         autoindex on;
     }
 }
 """
 
 PROCESS_NAME = 'nginx'
 
+
 class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
     @classmethod
     def setUpClass(cls):
         super(TestHTTPSService, cls).setUpClass()
 
         # ensure we can also run this test on a live system - so lets clean
         # out the current configuration :)
         cls.cli_delete(cls, base_path)
         cls.cli_delete(cls, pki_base)
 
     @classmethod
     def tearDownClass(cls):
         super(TestHTTPSService, cls).tearDownClass()
         call(f'sudo rm -f {nginx_tmp_site}')
 
     def tearDown(self):
         self.cli_delete(base_path)
         self.cli_delete(pki_base)
         self.cli_commit()
 
         # Check for stopped  process
         self.assertFalse(process_named_running(PROCESS_NAME))
 
     def test_certificate(self):
         cert_name = 'test_https'
         dh_name = 'dh-test'
 
         self.cli_set(base_path + ['certificates', 'certificate', cert_name])
         # verify() - certificates do not exist (yet)
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
-        self.cli_set(pki_base + ['certificate', cert_name, 'certificate', cert_data.replace('\n','')])
-        self.cli_set(pki_base + ['certificate', cert_name, 'private', 'key', key_data.replace('\n','')])
+        self.cli_set(
+            pki_base
+            + ['certificate', cert_name, 'certificate', cert_data.replace('\n', '')]
+        )
+        self.cli_set(
+            pki_base
+            + ['certificate', cert_name, 'private', 'key', key_data.replace('\n', '')]
+        )
 
         self.cli_set(base_path + ['certificates', 'dh-params', dh_name])
         # verify() - dh-params do not exist (yet)
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
 
-        self.cli_set(pki_base + ['dh', dh_name, 'parameters', dh_1024.replace('\n','')])
+        self.cli_set(
+            pki_base + ['dh', dh_name, 'parameters', dh_1024.replace('\n', '')]
+        )
         # verify() - dh-param minimum length is 2048 bit
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
-        self.cli_set(pki_base + ['dh', dh_name, 'parameters', dh_2048.replace('\n','')])
+        self.cli_set(
+            pki_base + ['dh', dh_name, 'parameters', dh_2048.replace('\n', '')]
+        )
 
         self.cli_commit()
         self.assertTrue(process_named_running(PROCESS_NAME))
         self.debug = False
 
     def test_api_missing_keys(self):
         self.cli_set(base_path + ['api'])
         self.assertRaises(ConfigSessionError, self.cli_commit)
 
     def test_api_incomplete_key(self):
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01'])
         self.assertRaises(ConfigSessionError, self.cli_commit)
 
     @ignore_warning(InsecureRequestWarning)
     def test_api_auth(self):
         address = '127.0.0.1'
         port = default_value(base_path + ['port'])
 
         key = 'MySuperSecretVyOS'
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
 
+        self.cli_set(base_path + ['api', 'rest'])
+
         self.cli_set(base_path + ['listen-address', address])
 
         self.cli_commit()
 
         nginx_config = read_file('/etc/nginx/sites-enabled/default')
         self.assertIn(f'listen {address}:{port} ssl;', nginx_config)
-        self.assertIn(f'ssl_protocols TLSv1.2 TLSv1.3;', nginx_config) # default
+        self.assertIn('ssl_protocols TLSv1.2 TLSv1.3;', nginx_config)  # default
 
         url = f'https://{address}/retrieve'
         payload = {'data': '{"op": "showConfig", "path": []}', 'key': f'{key}'}
         headers = {}
         r = request('POST', url, verify=False, headers=headers, data=payload)
         # Must get HTTP code 200 on success
         self.assertEqual(r.status_code, 200)
 
         payload_invalid = {'data': '{"op": "showConfig", "path": []}', 'key': 'invalid'}
         r = request('POST', url, verify=False, headers=headers, data=payload_invalid)
         # Must get HTTP code 401 on invalid key (Unauthorized)
         self.assertEqual(r.status_code, 401)
 
         payload_no_key = {'data': '{"op": "showConfig", "path": []}'}
         r = request('POST', url, verify=False, headers=headers, data=payload_no_key)
         # Must get HTTP code 401 on missing key (Unauthorized)
         self.assertEqual(r.status_code, 401)
 
         # Check path config
-        payload = {'data': '{"op": "showConfig", "path": ["system", "login"]}', 'key': f'{key}'}
+        payload = {
+            'data': '{"op": "showConfig", "path": ["system", "login"]}',
+            'key': f'{key}',
+        }
         r = request('POST', url, verify=False, headers=headers, data=payload)
         response = r.json()
         vyos_user_exists = 'vyos' in response.get('data', {}).get('user', {})
-        self.assertTrue(vyos_user_exists, "The 'vyos' user does not exist in the response.")
+        self.assertTrue(
+            vyos_user_exists, "The 'vyos' user does not exist in the response."
+        )
 
         # GraphQL auth test: a missing key will return status code 400, as
         # 'key' is a non-nullable field in the schema; an incorrect key is
         # caught by the resolver, and returns success 'False', so one must
         # check the return value.
 
         self.cli_set(base_path + ['api', 'graphql'])
         self.cli_commit()
 
         graphql_url = f'https://{address}/graphql'
 
         query_valid_key = f"""
         {{
           SystemStatus (data: {{key: "{key}"}}) {{
             success
             errors
             data {{
               result
             }}
           }}
         }}
         """
 
-        r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_valid_key})
+        r = request(
+            'POST',
+            graphql_url,
+            verify=False,
+            headers=headers,
+            json={'query': query_valid_key},
+        )
         success = r.json()['data']['SystemStatus']['success']
         self.assertTrue(success)
 
         query_invalid_key = """
         {
           SystemStatus (data: {key: "invalid"}) {
             success
             errors
             data {
               result
             }
           }
         }
         """
 
-        r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_invalid_key})
+        r = request(
+            'POST',
+            graphql_url,
+            verify=False,
+            headers=headers,
+            json={'query': query_invalid_key},
+        )
         success = r.json()['data']['SystemStatus']['success']
         self.assertFalse(success)
 
         query_no_key = """
         {
           SystemStatus (data: {}) {
             success
             errors
             data {
               result
             }
           }
         }
         """
 
-        r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_no_key})
+        r = request(
+            'POST',
+            graphql_url,
+            verify=False,
+            headers=headers,
+            json={'query': query_no_key},
+        )
         success = r.json()['data']['SystemStatus']['success']
         self.assertFalse(success)
 
         # GraphQL token authentication test: request token; pass in header
         # of query.
 
         self.cli_set(base_path + ['api', 'graphql', 'authentication', 'type', 'token'])
         self.cli_commit()
 
         mutation = """
         mutation {
           AuthToken (data: {username: "vyos", password: "vyos"}) {
             success
             errors
             data {
               result
             }
           }
         }
         """
-        r = request('POST', graphql_url, verify=False, headers=headers, json={'query': mutation})
+        r = request(
+            'POST', graphql_url, verify=False, headers=headers, json={'query': mutation}
+        )
 
         token = r.json()['data']['AuthToken']['data']['result']['token']
 
         headers = {'Authorization': f'Bearer {token}'}
 
         query = """
         {
           ShowVersion (data: {}) {
             success
             errors
             op_mode_error {
               name
               message
               vyos_code
             }
             data {
               result
             }
           }
         }
         """
 
-        r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query})
+        r = request(
+            'POST', graphql_url, verify=False, headers=headers, json={'query': query}
+        )
         success = r.json()['data']['ShowVersion']['success']
         self.assertTrue(success)
 
     @ignore_warning(InsecureRequestWarning)
     def test_api_add_delete(self):
         address = '127.0.0.1'
         key = 'VyOS-key'
         url = f'https://{address}/retrieve'
         payload = {'data': '{"op": "showConfig", "path": []}', 'key': f'{key}'}
         headers = {}
 
         self.cli_set(base_path)
         self.cli_commit()
 
         r = request('POST', url, verify=False, headers=headers, data=payload)
         # api not configured; expect 503
         self.assertEqual(r.status_code, 503)
 
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+        self.cli_set(base_path + ['api', 'rest'])
         self.cli_commit()
         sleep(2)
 
         r = request('POST', url, verify=False, headers=headers, data=payload)
         # api configured; expect 200
         self.assertEqual(r.status_code, 200)
 
         self.cli_delete(base_path + ['api'])
         self.cli_commit()
 
         r = request('POST', url, verify=False, headers=headers, data=payload)
         # api deleted; expect 503
         self.assertEqual(r.status_code, 503)
 
     @ignore_warning(InsecureRequestWarning)
     def test_api_show(self):
         address = '127.0.0.1'
         key = 'VyOS-key'
         url = f'https://{address}/show'
         headers = {}
 
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+        self.cli_set(base_path + ['api', 'rest'])
         self.cli_commit()
 
         payload = {
             'data': '{"op": "show", "path": ["system", "image"]}',
             'key': f'{key}',
         }
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 200)
 
     @ignore_warning(InsecureRequestWarning)
     def test_api_generate(self):
         address = '127.0.0.1'
         key = 'VyOS-key'
         url = f'https://{address}/generate'
         headers = {}
 
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+        self.cli_set(base_path + ['api', 'rest'])
         self.cli_commit()
 
         payload = {
             'data': '{"op": "generate", "path": ["macsec", "mka", "cak", "gcm-aes-256"]}',
             'key': f'{key}',
         }
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 200)
 
     @ignore_warning(InsecureRequestWarning)
     def test_api_configure(self):
         address = '127.0.0.1'
         key = 'VyOS-key'
         url = f'https://{address}/configure'
         headers = {}
         conf_interface = 'dum0'
         conf_address = '192.0.2.44/32'
 
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+        self.cli_set(base_path + ['api', 'rest'])
         self.cli_commit()
 
         payload_path = [
-            "interfaces",
-            "dummy",
-            f"{conf_interface}",
-            "address",
-            f"{conf_address}",
+            'interfaces',
+            'dummy',
+            f'{conf_interface}',
+            'address',
+            f'{conf_address}',
         ]
 
-        payload = {'data': json.dumps({"op": "set", "path": payload_path}), 'key': key}
+        payload = {'data': json.dumps({'op': 'set', 'path': payload_path}), 'key': key}
 
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 200)
 
     @ignore_warning(InsecureRequestWarning)
     def test_api_config_file(self):
         address = '127.0.0.1'
         key = 'VyOS-key'
         url = f'https://{address}/config-file'
         headers = {}
 
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+        self.cli_set(base_path + ['api', 'rest'])
         self.cli_commit()
 
         payload = {
             'data': '{"op": "save"}',
             'key': f'{key}',
         }
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 200)
 
     @ignore_warning(InsecureRequestWarning)
     def test_api_reset(self):
         address = '127.0.0.1'
         key = 'VyOS-key'
         url = f'https://{address}/reset'
         headers = {}
 
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+        self.cli_set(base_path + ['api', 'rest'])
         self.cli_commit()
 
         payload = {
             'data': '{"op": "reset", "path": ["ip", "arp", "table"]}',
             'key': f'{key}',
         }
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 200)
 
     @ignore_warning(InsecureRequestWarning)
     def test_api_image(self):
         address = '127.0.0.1'
         key = 'VyOS-key'
         url = f'https://{address}/image'
         headers = {}
 
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+        self.cli_set(base_path + ['api', 'rest'])
         self.cli_commit()
 
         payload = {
             'data': '{"op": "add"}',
             'key': f'{key}',
         }
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 400)
         self.assertIn('Missing required field "url"', r.json().get('error'))
 
         payload = {
             'data': '{"op": "delete"}',
             'key': f'{key}',
         }
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 400)
         self.assertIn('Missing required field "name"', r.json().get('error'))
 
         payload = {
             'data': '{"op": "set_default"}',
             'key': f'{key}',
         }
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 400)
         self.assertIn('Missing required field "name"', r.json().get('error'))
 
         payload = {
             'data': '{"op": "show"}',
             'key': f'{key}',
         }
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 200)
 
     @ignore_warning(InsecureRequestWarning)
     def test_api_config_file_load_http(self):
         # Test load config from HTTP URL
         address = '127.0.0.1'
         key = 'VyOS-key'
         url = f'https://{address}/config-file'
         url_config = f'https://{address}/configure'
         headers = {}
 
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+        self.cli_set(base_path + ['api', 'rest'])
         self.cli_commit()
 
         # load config via HTTP requires nginx config
         call(f'sudo touch {nginx_tmp_site}')
         call(f'sudo chmod 666 {nginx_tmp_site}')
         write_file(nginx_tmp_site, nginx_conf_smoketest)
         call('sudo systemctl reload nginx')
 
         # save config
         payload = {
             'data': '{"op": "save", "file": "/tmp/tmp-config.boot"}',
             'key': f'{key}',
         }
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 200)
 
         # change config
         payload = {
             'data': '{"op": "set", "path": ["interfaces", "dummy", "dum1", "address", "192.0.2.31/32"]}',
             'key': f'{key}',
         }
         r = request('POST', url_config, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 200)
 
         # load config from URL
         payload = {
             'data': '{"op": "load", "file": "http://localhost:8000/tmp-config.boot"}',
             'key': f'{key}',
         }
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 200)
 
         # cleanup tmp nginx conf
         call(f'sudo rm -f {nginx_tmp_site}')
         call('sudo systemctl reload nginx')
 
+
 if __name__ == '__main__':
     unittest.main(verbosity=5)
diff --git a/src/migration-scripts/https/6-to-7 b/src/migration-scripts/https/6-to-7
new file mode 100644
index 000000000..571f3b6ae
--- /dev/null
+++ b/src/migration-scripts/https/6-to-7
@@ -0,0 +1,43 @@
+# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library.  If not, see <http://www.gnu.org/licenses/>.
+
+# T6736: move REST API to distinct node
+
+
+from vyos.configtree import ConfigTree
+
+
+base = ['service', 'https', 'api']
+
+def migrate(config: ConfigTree) -> None:
+    if not config.exists(base):
+        # Nothing to do
+        return
+
+    # Move REST API configuration to new node
+    # REST API was previously enabled if base path exists
+    config.set(['service', 'https', 'api', 'rest'])
+    for entry in ('debug', 'strict'):
+        if config.exists(base + [entry]):
+            config.set(base + ['rest', entry])
+            config.delete(base + [entry])
+
+    # Move CORS settings under GraphQL
+    # CORS is not implemented for REST API
+    if config.exists(base + ['cors']):
+        old_base = base + ['cors']
+        new_base = base + ['graphql', 'cors']
+        config.copy(old_base, new_base)
+        config.delete(old_base)
diff --git a/src/services/api/__init__.py b/src/services/api/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py
index ef4966466..ebf745f32 100644
--- a/src/services/api/graphql/bindings.py
+++ b/src/services/api/graphql/bindings.py
@@ -1,36 +1,52 @@
-# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2021-2024 VyOS maintainers and contributors <maintainers@vyos.io>
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
 # License as published by the Free Software Foundation; either
 # version 2.1 of the License, or (at your option) any later version.
 #
 # This library is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # Lesser General Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public License
 # along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
+
 import vyos.defaults
-from . graphql.queries import query
-from . graphql.mutations import mutation
-from . graphql.directives import directives_dict
-from . graphql.errors import op_mode_error
-from . graphql.auth_token_mutation import auth_token_mutation
-from . libs.token_auth import init_secret
-from . import state
-from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers
+
+from ariadne import make_executable_schema
+from ariadne import load_schema_from_path
+from ariadne import snake_case_fallback_resolvers
+
+from .graphql.queries import query
+from .graphql.mutations import mutation
+from .graphql.directives import directives_dict
+from .graphql.errors import op_mode_error
+from .graphql.auth_token_mutation import auth_token_mutation
+from .libs.token_auth import init_secret
+
+from ..session import SessionState
+
 
 def generate_schema():
+    state = SessionState()
     api_schema_dir = vyos.defaults.directories['api_schema']
 
-    if state.settings['app'].state.vyos_auth_type == 'token':
+    if state.auth_type == 'token':
         init_secret()
 
     type_defs = load_schema_from_path(api_schema_dir)
 
-    schema = make_executable_schema(type_defs, query, op_mode_error, mutation, auth_token_mutation, snake_case_fallback_resolvers, directives=directives_dict)
+    schema = make_executable_schema(
+        type_defs,
+        query,
+        op_mode_error,
+        mutation,
+        auth_token_mutation,
+        snake_case_fallback_resolvers,
+        directives=directives_dict,
+    )
 
     return schema
diff --git a/src/services/api/graphql/graphql/auth_token_mutation.py b/src/services/api/graphql/graphql/auth_token_mutation.py
index a53fa4d60..c74364603 100644
--- a/src/services/api/graphql/graphql/auth_token_mutation.py
+++ b/src/services/api/graphql/graphql/auth_token_mutation.py
@@ -1,61 +1,56 @@
 # Copyright 2022-2024 VyOS maintainers and contributors <maintainers@vyos.io>
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
 # License as published by the Free Software Foundation; either
 # version 2.1 of the License, or (at your option) any later version.
 #
 # This library is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # Lesser General Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public License
 # along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
 import datetime
 from typing import Any
 from typing import Dict
 from ariadne import ObjectType
 from graphql import GraphQLResolveInfo
 
-from .. libs.token_auth import generate_token
-from .. session.session import get_user_info
-from .. import state
+from ..libs.token_auth import generate_token
+from ..session.session import get_user_info
+from ...session import SessionState
+
+auth_token_mutation = ObjectType('Mutation')
 
-auth_token_mutation = ObjectType("Mutation")
 
 @auth_token_mutation.field('AuthToken')
 def auth_token_resolver(obj: Any, info: GraphQLResolveInfo, data: Dict):
     # non-nullable fields
     user = data['username']
     passwd = data['password']
 
-    secret = state.settings['secret']
-    exp_interval = int(state.settings['app'].state.vyos_token_exp)
-    expiration = (datetime.datetime.now(tz=datetime.timezone.utc) +
-                  datetime.timedelta(seconds=exp_interval))
+    state = SessionState()
+
+    secret = getattr(state, 'secret', '')
+    exp_interval = int(state.token_exp)
+    expiration = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(
+        seconds=exp_interval
+    )
 
     res = generate_token(user, passwd, secret, expiration)
     try:
         res |= get_user_info(user)
     except ValueError:
         # non-existent user already caught
         pass
     if 'token' in res:
         data['result'] = res
-        return {
-            "success": True,
-            "data": data
-        }
+        return {'success': True, 'data': data}
 
     if 'errors' in res:
-        return {
-            "success": False,
-            "errors": res['errors']
-        }
-
-    return {
-        "success": False,
-        "errors": ['token generation failed']
-    }
+        return {'success': False, 'errors': res['errors']}
+
+    return {'success': False, 'errors': ['token generation failed']}
diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py
index d115a8e94..0b391c070 100644
--- a/src/services/api/graphql/graphql/mutations.py
+++ b/src/services/api/graphql/graphql/mutations.py
@@ -1,139 +1,135 @@
 # Copyright 2021-2024 VyOS maintainers and contributors <maintainers@vyos.io>
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
 # License as published by the Free Software Foundation; either
 # version 2.1 of the License, or (at your option) any later version.
 #
 # This library is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # Lesser General Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public License
 # along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
 from importlib import import_module
-from ariadne import ObjectType, convert_camel_case_to_snake
-from makefun import with_signature
 
 # used below by func_sig
-from typing import Any, Dict, Optional # pylint: disable=W0611
-from graphql import GraphQLResolveInfo # pylint: disable=W0611
+from typing import Any, Dict, Optional  # pylint: disable=W0611 # noqa: F401
+from graphql import GraphQLResolveInfo  # pylint: disable=W0611 # noqa: F401
+
+from ariadne import ObjectType, convert_camel_case_to_snake
+from makefun import with_signature
 
-from .. import state
-from .. libs import key_auth
-from api.graphql.session.session import Session
-from api.graphql.session.errors.op_mode_errors import op_mode_err_msg, op_mode_err_code
 from vyos.opmode import Error as OpModeError
 
-mutation = ObjectType("Mutation")
+from ...session import SessionState
+from ..libs import key_auth
+from ..session.session import Session
+from ..session.errors.op_mode_errors import op_mode_err_msg, op_mode_err_code
+
+mutation = ObjectType('Mutation')
+
 
 def make_mutation_resolver(mutation_name, class_name, session_func):
     """Dynamically generate a resolver for the mutation named in the
     schema by 'mutation_name'.
 
     Dynamic generation is provided using the package 'makefun' (via the
     decorator 'with_signature'), which provides signature-preserving
     function wrappers; it provides several improvements over, say,
     functools.wraps.
 
     :raise Exception:
         raising ConfigErrors, or internal errors
     """
 
     func_base_name = convert_camel_case_to_snake(class_name)
     resolver_name = f'resolve_{func_base_name}'
     func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Optional[Dict]=None)'
+    state = SessionState()
 
     @mutation.field(mutation_name)
     @with_signature(func_sig, func_name=resolver_name)
     async def func_impl(*args, **kwargs):
         try:
-            auth_type = state.settings['app'].state.vyos_auth_type
+            auth_type = state.auth_type
 
             if auth_type == 'key':
                 data = kwargs['data']
                 key = data['key']
 
                 auth = key_auth.auth_required(key)
                 if auth is None:
-                    return {
-                         "success": False,
-                         "errors": ['invalid API key']
-                    }
+                    return {'success': False, 'errors': ['invalid API key']}
 
                 # We are finished with the 'key' entry, and may remove so as to
                 # pass the rest of data (if any) to function.
                 del data['key']
 
             elif auth_type == 'token':
                 data = kwargs['data']
                 if data is None:
                     data = {}
                 info = kwargs['info']
                 user = info.context.get('user')
                 if user is None:
                     error = info.context.get('error')
                     if error is not None:
-                        return {
-                            "success": False,
-                            "errors": [error]
-                        }
-                    return {
-                        "success": False,
-                        "errors": ['not authenticated']
-                    }
+                        return {'success': False, 'errors': [error]}
+                    return {'success': False, 'errors': ['not authenticated']}
             else:
                 # AtrributeError will have already been raised if no
-                # vyos_auth_type; validation and defaultValue ensure it is
+                # auth_type; validation and defaultValue ensure it is
                 # one of the previous cases, so this is never reached.
                 pass
 
-            session = state.settings['app'].state.vyos_session
+            session = state.session
 
             # one may override the session functions with a local subclass
             try:
                 mod = import_module(f'api.graphql.session.override.{func_base_name}')
                 klass = getattr(mod, class_name)
             except ImportError:
                 # otherwise, dynamically generate subclass to invoke subclass
                 # name based functions
                 klass = type(class_name, (Session,), {})
             k = klass(session, data)
             method = getattr(k, session_func)
             result = method()
             data['result'] = result
 
-            return {
-                "success": True,
-                "data": data
-            }
+            return {'success': True, 'data': data}
         except OpModeError as e:
             typename = type(e).__name__
             msg = str(e)
             return {
-                "success": False,
-                "errore": ['op_mode_error'],
-                "op_mode_error": {"name": f"{typename}",
-                                 "message": msg if msg else op_mode_err_msg.get(typename, "Unknown"),
-                                 "vyos_code": op_mode_err_code.get(typename, 9999)}
+                'success': False,
+                'errore': ['op_mode_error'],
+                'op_mode_error': {
+                    'name': f'{typename}',
+                    'message': msg if msg else op_mode_err_msg.get(typename, 'Unknown'),
+                    'vyos_code': op_mode_err_code.get(typename, 9999),
+                },
             }
         except Exception as error:
-            return {
-                "success": False,
-                "errors": [repr(error)]
-            }
+            return {'success': False, 'errors': [repr(error)]}
 
     return func_impl
 
+
 def make_config_session_mutation_resolver(mutation_name):
-    return make_mutation_resolver(mutation_name, mutation_name,
-                                  convert_camel_case_to_snake(mutation_name))
+    return make_mutation_resolver(
+        mutation_name, mutation_name, convert_camel_case_to_snake(mutation_name)
+    )
+
 
 def make_gen_op_mutation_resolver(mutation_name):
     return make_mutation_resolver(mutation_name, mutation_name, 'gen_op_mutation')
 
+
 def make_composite_mutation_resolver(mutation_name):
-    return make_mutation_resolver(mutation_name, mutation_name,
-                                  convert_camel_case_to_snake(mutation_name))
+    return make_mutation_resolver(
+        mutation_name, mutation_name, convert_camel_case_to_snake(mutation_name)
+    )
diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py
index 717098259..9303fe909 100644
--- a/src/services/api/graphql/graphql/queries.py
+++ b/src/services/api/graphql/graphql/queries.py
@@ -1,139 +1,135 @@
 # Copyright 2021-2024 VyOS maintainers and contributors <maintainers@vyos.io>
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
 # License as published by the Free Software Foundation; either
 # version 2.1 of the License, or (at your option) any later version.
 #
 # This library is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # Lesser General Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public License
 # along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
 from importlib import import_module
-from ariadne import ObjectType, convert_camel_case_to_snake
-from makefun import with_signature
 
 # used below by func_sig
-from typing import Any, Dict, Optional # pylint: disable=W0611
-from graphql import GraphQLResolveInfo # pylint: disable=W0611
+from typing import Any, Dict, Optional  # pylint: disable=W0611 # noqa: F401
+from graphql import GraphQLResolveInfo  # pylint: disable=W0611 # noqa: F401
+
+from ariadne import ObjectType, convert_camel_case_to_snake
+from makefun import with_signature
 
-from .. import state
-from .. libs import key_auth
-from api.graphql.session.session import Session
-from api.graphql.session.errors.op_mode_errors import op_mode_err_msg, op_mode_err_code
 from vyos.opmode import Error as OpModeError
 
-query = ObjectType("Query")
+from ...session import SessionState
+from ..libs import key_auth
+from ..session.session import Session
+from ..session.errors.op_mode_errors import op_mode_err_msg, op_mode_err_code
+
+query = ObjectType('Query')
+
 
 def make_query_resolver(query_name, class_name, session_func):
     """Dynamically generate a resolver for the query named in the
     schema by 'query_name'.
 
     Dynamic generation is provided using the package 'makefun' (via the
     decorator 'with_signature'), which provides signature-preserving
     function wrappers; it provides several improvements over, say,
     functools.wraps.
 
     :raise Exception:
         raising ConfigErrors, or internal errors
     """
 
     func_base_name = convert_camel_case_to_snake(class_name)
     resolver_name = f'resolve_{func_base_name}'
     func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Optional[Dict]=None)'
+    state = SessionState()
 
     @query.field(query_name)
     @with_signature(func_sig, func_name=resolver_name)
     async def func_impl(*args, **kwargs):
         try:
-            auth_type = state.settings['app'].state.vyos_auth_type
+            auth_type = state.auth_type
 
             if auth_type == 'key':
                 data = kwargs['data']
                 key = data['key']
 
                 auth = key_auth.auth_required(key)
                 if auth is None:
-                    return {
-                         "success": False,
-                         "errors": ['invalid API key']
-                    }
+                    return {'success': False, 'errors': ['invalid API key']}
 
                 # We are finished with the 'key' entry, and may remove so as to
                 # pass the rest of data (if any) to function.
                 del data['key']
 
             elif auth_type == 'token':
                 data = kwargs['data']
                 if data is None:
                     data = {}
                 info = kwargs['info']
                 user = info.context.get('user')
                 if user is None:
                     error = info.context.get('error')
                     if error is not None:
-                        return {
-                            "success": False,
-                            "errors": [error]
-                        }
-                    return {
-                        "success": False,
-                        "errors": ['not authenticated']
-                    }
+                        return {'success': False, 'errors': [error]}
+                    return {'success': False, 'errors': ['not authenticated']}
             else:
                 # AtrributeError will have already been raised if no
-                # vyos_auth_type; validation and defaultValue ensure it is
+                # auth_type; validation and defaultValue ensure it is
                 # one of the previous cases, so this is never reached.
                 pass
 
-            session = state.settings['app'].state.vyos_session
+            session = state.session
 
             # one may override the session functions with a local subclass
             try:
                 mod = import_module(f'api.graphql.session.override.{func_base_name}')
                 klass = getattr(mod, class_name)
             except ImportError:
                 # otherwise, dynamically generate subclass to invoke subclass
                 # name based functions
                 klass = type(class_name, (Session,), {})
             k = klass(session, data)
             method = getattr(k, session_func)
             result = method()
             data['result'] = result
 
-            return {
-                "success": True,
-                "data": data
-            }
+            return {'success': True, 'data': data}
         except OpModeError as e:
             typename = type(e).__name__
             msg = str(e)
             return {
-                "success": False,
-                "errors": ['op_mode_error'],
-                "op_mode_error": {"name": f"{typename}",
-                                 "message": msg if msg else op_mode_err_msg.get(typename, "Unknown"),
-                                 "vyos_code": op_mode_err_code.get(typename, 9999)}
+                'success': False,
+                'errors': ['op_mode_error'],
+                'op_mode_error': {
+                    'name': f'{typename}',
+                    'message': msg if msg else op_mode_err_msg.get(typename, 'Unknown'),
+                    'vyos_code': op_mode_err_code.get(typename, 9999),
+                },
             }
         except Exception as error:
-            return {
-                "success": False,
-                "errors": [repr(error)]
-            }
+            return {'success': False, 'errors': [repr(error)]}
 
     return func_impl
 
+
 def make_config_session_query_resolver(query_name):
-    return make_query_resolver(query_name, query_name,
-                               convert_camel_case_to_snake(query_name))
+    return make_query_resolver(
+        query_name, query_name, convert_camel_case_to_snake(query_name)
+    )
+
 
 def make_gen_op_query_resolver(query_name):
     return make_query_resolver(query_name, query_name, 'gen_op_query')
 
+
 def make_composite_query_resolver(query_name):
-    return make_query_resolver(query_name, query_name,
-                               convert_camel_case_to_snake(query_name))
+    return make_query_resolver(
+        query_name, query_name, convert_camel_case_to_snake(query_name)
+    )
diff --git a/src/services/api/graphql/libs/__init__.py b/src/services/api/graphql/libs/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/services/api/graphql/libs/key_auth.py b/src/services/api/graphql/libs/key_auth.py
index 2db0f7d48..ffd7f32b2 100644
--- a/src/services/api/graphql/libs/key_auth.py
+++ b/src/services/api/graphql/libs/key_auth.py
@@ -1,18 +1,36 @@
+# Copyright 2021-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from ...session import SessionState
 
-from .. import state
 
 def check_auth(key_list, key):
     if not key_list:
         return None
     key_id = None
     for k in key_list:
         if k['key'] == key:
             key_id = k['id']
     return key_id
 
+
 def auth_required(key):
+    state = SessionState()
     api_keys = None
-    api_keys = state.settings['app'].state.vyos_keys
+    api_keys = state.keys
     key_id = check_auth(api_keys, key)
-    state.settings['app'].state.vyos_id = key_id
+    state.id = key_id
     return key_id
diff --git a/src/services/api/graphql/libs/token_auth.py b/src/services/api/graphql/libs/token_auth.py
index 8585485c9..4f743a096 100644
--- a/src/services/api/graphql/libs/token_auth.py
+++ b/src/services/api/graphql/libs/token_auth.py
@@ -1,70 +1,91 @@
+# Copyright 2021-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library.  If not, see <http://www.gnu.org/licenses/>.
+
+
 import jwt
 import uuid
 import pam
 from secrets import token_hex
 
-from .. import state
+from ...session import SessionState
+
 
 def _check_passwd_pam(username: str, passwd: str) -> bool:
     if pam.authenticate(username, passwd):
         return True
     return False
 
+
 def init_secret():
-    length = int(state.settings['app'].state.vyos_secret_len)
+    state = SessionState()
+    length = int(state.secret_len)
     secret = token_hex(length)
-    state.settings['secret'] = secret
+    state.secret = secret
+
 
 def generate_token(user: str, passwd: str, secret: str, exp: int) -> dict:
     if user is None or passwd is None:
         return {}
+    state = SessionState()
     if _check_passwd_pam(user, passwd):
-        app = state.settings['app']
         try:
-            users = app.state.vyos_token_users
+            users = state.token_users
         except AttributeError:
-            app.state.vyos_token_users = {}
-            users = app.state.vyos_token_users
+            users = state.token_users = {}
         user_id = uuid.uuid1().hex
         payload_data = {'iss': user, 'sub': user_id, 'exp': exp}
-        secret = state.settings.get('secret')
+        secret = getattr(state, 'secret', None)
         if secret is None:
-            return {"errors": ['missing secret']}
-        token = jwt.encode(payload=payload_data, key=secret, algorithm="HS256")
+            return {'errors': ['missing secret']}
+        token = jwt.encode(payload=payload_data, key=secret, algorithm='HS256')
 
         users |= {user_id: user}
         return {'token': token}
     else:
-        return {"errors": ['failed pam authentication']}
+        return {'errors': ['failed pam authentication']}
+
 
 def get_user_context(request):
     context = {}
     context['request'] = request
     context['user'] = None
+    state = SessionState()
     if 'Authorization' in request.headers:
         auth = request.headers['Authorization']
         scheme, token = auth.split()
         if scheme.lower() != 'bearer':
             return context
 
         try:
-            secret = state.settings.get('secret')
-            payload = jwt.decode(token, secret, algorithms=["HS256"])
+            secret = getattr(state, 'secret', None)
+            payload = jwt.decode(token, secret, algorithms=['HS256'])
             user_id: str = payload.get('sub')
             if user_id is None:
                 return context
         except jwt.exceptions.ExpiredSignatureError:
             context['error'] = 'expired token'
             return context
         except jwt.PyJWTError:
             return context
         try:
-            users = state.settings['app'].state.vyos_token_users
+            users = state.token_users
         except AttributeError:
             return context
 
         user = users.get(user_id)
         if user is not None:
             context['user'] = user
 
     return context
diff --git a/src/services/api/graphql/routers.py b/src/services/api/graphql/routers.py
new file mode 100644
index 000000000..ed3ee1e8c
--- /dev/null
+++ b/src/services/api/graphql/routers.py
@@ -0,0 +1,77 @@
+# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library.  If not, see <http://www.gnu.org/licenses/>.
+
+# pylint: disable=import-outside-toplevel
+
+
+import typing
+
+from ariadne.asgi import GraphQL
+from starlette.middleware.cors import CORSMiddleware
+
+
+if typing.TYPE_CHECKING:
+    from fastapi import FastAPI
+
+
+def graphql_init(app: 'FastAPI'):
+    from ..session import SessionState
+    from .libs.token_auth import get_user_context
+
+    state = SessionState()
+
+    # import after initializaion of state
+    from .bindings import generate_schema
+
+    schema = generate_schema()
+
+    in_spec = state.introspection
+
+    # remove route and reinstall below, for any changes; alternatively, test
+    # for config_diff before proceeding
+    graphql_clear(app)
+
+    if state.origins:
+        origins = state.origins
+        app.add_route(
+            '/graphql',
+            CORSMiddleware(
+                GraphQL(
+                    schema,
+                    context_value=get_user_context,
+                    debug=True,
+                    introspection=in_spec,
+                ),
+                allow_origins=origins,
+                allow_methods=('GET', 'POST', 'OPTIONS'),
+                allow_headers=('Authorization',),
+            ),
+        )
+    else:
+        app.add_route(
+            '/graphql',
+            GraphQL(
+                schema,
+                context_value=get_user_context,
+                debug=True,
+                introspection=in_spec,
+            ),
+        )
+
+
+def graphql_clear(app: 'FastAPI'):
+    for r in app.routes:
+        if r.path == '/graphql':
+            app.routes.remove(r)
diff --git a/src/services/api/graphql/session/session.py b/src/services/api/graphql/session/session.py
index 6ae44b9bf..619534f43 100644
--- a/src/services/api/graphql/session/session.py
+++ b/src/services/api/graphql/session/session.py
@@ -1,211 +1,218 @@
 # Copyright 2021-2024 VyOS maintainers and contributors <maintainers@vyos.io>
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
 # License as published by the Free Software Foundation; either
 # version 2.1 of the License, or (at your option) any later version.
 #
 # This library is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # Lesser General Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public License
 # along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
 import json
 
 from ariadne import convert_camel_case_to_snake
 
 from vyos.config import Config
 from vyos.configtree import ConfigTree
 from vyos.defaults import directories
 from vyos.opmode import Error as OpModeError
 
 from api.graphql.libs.op_mode import load_op_mode_as_module, split_compound_op_mode_name
 from api.graphql.libs.op_mode import normalize_output
 
 op_mode_include_file = os.path.join(directories['data'], 'op-mode-standardized.json')
 
-def get_config_dict(path=[], effective=False, key_mangling=None,
-                     get_first_key=False, no_multi_convert=False,
-                     no_tag_node_value_mangle=False):
+
+def get_config_dict(
+    path=[],
+    effective=False,
+    key_mangling=None,
+    get_first_key=False,
+    no_multi_convert=False,
+    no_tag_node_value_mangle=False,
+):
     config = Config()
-    return config.get_config_dict(path=path, effective=effective,
-                                  key_mangling=key_mangling,
-                                  get_first_key=get_first_key,
-                                  no_multi_convert=no_multi_convert,
-                                  no_tag_node_value_mangle=no_tag_node_value_mangle)
+    return config.get_config_dict(
+        path=path,
+        effective=effective,
+        key_mangling=key_mangling,
+        get_first_key=get_first_key,
+        no_multi_convert=no_multi_convert,
+        no_tag_node_value_mangle=no_tag_node_value_mangle,
+    )
+
 
 def get_user_info(user):
     user_info = {}
-    info = get_config_dict(['system', 'login', 'user', user],
-                           get_first_key=True)
+    info = get_config_dict(['system', 'login', 'user', user], get_first_key=True)
     if not info:
-        raise ValueError("No such user")
+        raise ValueError('No such user')
 
     user_info['user'] = user
     user_info['full_name'] = info.get('full-name', '')
 
     return user_info
 
+
 class Session:
     """
     Wrapper for calling configsession functions based on GraphQL requests.
     Non-nullable fields in the respective schema allow avoiding a key check
     in 'data'.
     """
+
     def __init__(self, session, data):
         self._session = session
         self._data = data
         self._name = convert_camel_case_to_snake(type(self).__name__)
 
         try:
             with open(op_mode_include_file) as f:
                 self._op_mode_list = json.loads(f.read())
         except Exception:
             self._op_mode_list = None
 
     def show_config(self):
         session = self._session
         data = self._data
         out = ''
 
         try:
             out = session.show_config(data['path'])
             if data.get('config_format', '') == 'json':
                 config_tree = ConfigTree(out)
                 out = json.loads(config_tree.to_json())
         except Exception as error:
             raise error
 
         return out
 
     def save_config_file(self):
         session = self._session
         data = self._data
         if 'file_name' not in data or not data['file_name']:
             data['file_name'] = '/config/config.boot'
 
         try:
             session.save_config(data['file_name'])
         except Exception as error:
             raise error
 
     def load_config_file(self):
         session = self._session
         data = self._data
 
         try:
             session.load_config(data['file_name'])
             session.commit()
         except Exception as error:
             raise error
 
     def show(self):
         session = self._session
         data = self._data
         out = ''
 
         try:
             out = session.show(data['path'])
         except Exception as error:
             raise error
 
         return out
 
     def add_system_image(self):
         session = self._session
         data = self._data
 
         try:
             res = session.install_image(data['location'])
         except Exception as error:
             raise error
 
         return res
 
     def delete_system_image(self):
         session = self._session
         data = self._data
 
         try:
             res = session.remove_image(data['name'])
         except Exception as error:
             raise error
 
         return res
 
     def show_user_info(self):
-        session = self._session
         data = self._data
 
         user_info = {}
         user = data['user']
         try:
             user_info = get_user_info(user)
         except Exception as error:
             raise error
 
         return user_info
 
     def system_status(self):
-        import api.graphql.session.composite.system_status as system_status
+        from api.graphql.session.composite import system_status
 
         session = self._session
-        data = self._data
 
         status = {}
         status['host_name'] = session.show(['host', 'name']).strip()
         status['version'] = system_status.get_system_version()
         status['uptime'] = system_status.get_system_uptime()
         status['ram'] = system_status.get_system_ram_usage()
 
         return status
 
     def gen_op_query(self):
-        session = self._session
         data = self._data
         name = self._name
         op_mode_list = self._op_mode_list
 
         # handle the case that the op-mode file contains underscores:
         if op_mode_list is None:
             raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'")
         (func_name, scriptname) = split_compound_op_mode_name(name, op_mode_list)
         if scriptname == '':
             raise FileNotFoundError(f"No op-mode file named in string '{name}'")
 
         mod = load_op_mode_as_module(f'{scriptname}')
         func = getattr(mod, func_name)
         try:
             res = func(True, **data)
         except OpModeError as e:
             raise e
 
         res = normalize_output(res)
 
         return res
 
     def gen_op_mutation(self):
-        session = self._session
         data = self._data
         name = self._name
         op_mode_list = self._op_mode_list
 
         # handle the case that the op-mode file name contains underscores:
         if op_mode_list is None:
             raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'")
         (func_name, scriptname) = split_compound_op_mode_name(name, op_mode_list)
         if scriptname == '':
             raise FileNotFoundError(f"No op-mode file named in string '{name}'")
 
         mod = load_op_mode_as_module(f'{scriptname}')
         func = getattr(mod, func_name)
         try:
             res = func(**data)
         except OpModeError as e:
             raise e
 
         return res
diff --git a/src/services/api/graphql/state.py b/src/services/api/graphql/state.py
deleted file mode 100644
index 63db9f4ef..000000000
--- a/src/services/api/graphql/state.py
+++ /dev/null
@@ -1,4 +0,0 @@
-
-def init():
-    global settings
-    settings = {}
diff --git a/src/services/api/rest/__init__.py b/src/services/api/rest/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/services/api/rest/models.py b/src/services/api/rest/models.py
new file mode 100644
index 000000000..23ae9be9d
--- /dev/null
+++ b/src/services/api/rest/models.py
@@ -0,0 +1,299 @@
+# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library.  If not, see <http://www.gnu.org/licenses/>.
+
+
+# pylint: disable=too-few-public-methods
+
+import json
+from html import escape
+from enum import Enum
+from typing import List
+from typing import Union
+from typing import Dict
+from typing import Self
+
+from pydantic import BaseModel
+from pydantic import StrictStr
+from pydantic import field_validator
+from pydantic import model_validator
+from fastapi.responses import HTMLResponse
+
+
+def error(code, msg):
+    msg = escape(msg, quote=False)
+    resp = {'success': False, 'error': msg, 'data': None}
+    resp = json.dumps(resp)
+    return HTMLResponse(resp, status_code=code)
+
+
+def success(data):
+    resp = {'success': True, 'data': data, 'error': None}
+    resp = json.dumps(resp)
+    return HTMLResponse(resp)
+
+
+# Pydantic models for validation
+# Pydantic will cast when possible, so use StrictStr validators added as
+# needed for additional constraints
+# json_schema_extra adds anotations to OpenAPI to add examples
+
+
+class ApiModel(BaseModel):
+    key: StrictStr
+
+
+class BasePathModel(BaseModel):
+    op: StrictStr
+    path: List[StrictStr]
+
+    @field_validator('path')
+    @classmethod
+    def check_non_empty(cls, path: str) -> str:
+        if not len(path) > 0:
+            raise ValueError('path must be non-empty')
+        return path
+
+
+class BaseConfigureModel(BasePathModel):
+    value: StrictStr = None
+
+
+class ConfigureModel(ApiModel, BaseConfigureModel):
+    class Config:
+        json_schema_extra = {
+            'example': {
+                'key': 'id_key',
+                'op': 'set | delete | comment',
+                'path': ['config', 'mode', 'path'],
+            }
+        }
+
+
+class ConfigureListModel(ApiModel):
+    commands: List[BaseConfigureModel]
+
+    class Config:
+        json_schema_extra = {
+            'example': {
+                'key': 'id_key',
+                'commands': 'list of commands',
+            }
+        }
+
+
+class BaseConfigSectionModel(BasePathModel):
+    section: Dict
+
+
+class ConfigSectionModel(ApiModel, BaseConfigSectionModel):
+    pass
+
+
+class ConfigSectionListModel(ApiModel):
+    commands: List[BaseConfigSectionModel]
+
+
+class BaseConfigSectionTreeModel(BaseModel):
+    op: StrictStr
+    mask: Dict
+    config: Dict
+
+
+class ConfigSectionTreeModel(ApiModel, BaseConfigSectionTreeModel):
+    pass
+
+
+class RetrieveModel(ApiModel):
+    op: StrictStr
+    path: List[StrictStr]
+    configFormat: StrictStr = None
+
+    class Config:
+        json_schema_extra = {
+            'example': {
+                'key': 'id_key',
+                'op': 'returnValue | returnValues | exists | showConfig',
+                'path': ['config', 'mode', 'path'],
+                'configFormat': 'json (default) | json_ast | raw',
+            }
+        }
+
+
+class ConfigFileModel(ApiModel):
+    op: StrictStr
+    file: StrictStr = None
+
+    class Config:
+        json_schema_extra = {
+            'example': {
+                'key': 'id_key',
+                'op': 'save | load',
+                'file': 'filename',
+            }
+        }
+
+
+class ImageOp(str, Enum):
+    add = 'add'
+    delete = 'delete'
+    show = 'show'
+    set_default = 'set_default'
+
+
+class ImageModel(ApiModel):
+    op: ImageOp
+    url: StrictStr = None
+    name: StrictStr = None
+
+    @model_validator(mode='after')
+    def check_data(self) -> Self:
+        if self.op == 'add':
+            if not self.url:
+                raise ValueError('Missing required field "url"')
+        elif self.op in ['delete', 'set_default']:
+            if not self.name:
+                raise ValueError('Missing required field "name"')
+
+        return self
+
+    class Config:
+        json_schema_extra = {
+            'example': {
+                'key': 'id_key',
+                'op': 'add | delete | show | set_default',
+                'url': 'imagelocation',
+                'name': 'imagename',
+            }
+        }
+
+
+class ImportPkiModel(ApiModel):
+    op: StrictStr
+    path: List[StrictStr]
+    passphrase: StrictStr = None
+
+    class Config:
+        json_schema_extra = {
+            'example': {
+                'key': 'id_key',
+                'op': 'import_pki',
+                'path': ['op', 'mode', 'path'],
+                'passphrase': 'passphrase',
+            }
+        }
+
+
+class ContainerImageModel(ApiModel):
+    op: StrictStr
+    name: StrictStr = None
+
+    class Config:
+        json_schema_extra = {
+            'example': {
+                'key': 'id_key',
+                'op': 'add | delete | show',
+                'name': 'imagename',
+            }
+        }
+
+
+class GenerateModel(ApiModel):
+    op: StrictStr
+    path: List[StrictStr]
+
+    class Config:
+        json_schema_extra = {
+            'example': {
+                'key': 'id_key',
+                'op': 'generate',
+                'path': ['op', 'mode', 'path'],
+            }
+        }
+
+
+class ShowModel(ApiModel):
+    op: StrictStr
+    path: List[StrictStr]
+
+    class Config:
+        json_schema_extra = {
+            'example': {
+                'key': 'id_key',
+                'op': 'show',
+                'path': ['op', 'mode', 'path'],
+            }
+        }
+
+
+class RebootModel(ApiModel):
+    op: StrictStr
+    path: List[StrictStr]
+
+    class Config:
+        json_schema_extra = {
+            'example': {
+                'key': 'id_key',
+                'op': 'reboot',
+                'path': ['op', 'mode', 'path'],
+            }
+        }
+
+
+class ResetModel(ApiModel):
+    op: StrictStr
+    path: List[StrictStr]
+
+    class Config:
+        json_schema_extra = {
+            'example': {
+                'key': 'id_key',
+                'op': 'reset',
+                'path': ['op', 'mode', 'path'],
+            }
+        }
+
+
+class PoweroffModel(ApiModel):
+    op: StrictStr
+    path: List[StrictStr]
+
+    class Config:
+        json_schema_extra = {
+            'example': {
+                'key': 'id_key',
+                'op': 'poweroff',
+                'path': ['op', 'mode', 'path'],
+            }
+        }
+
+
+class Success(BaseModel):
+    success: bool
+    data: Union[str, bool, Dict]
+    error: str
+
+
+class Error(BaseModel):
+    success: bool = False
+    data: Union[str, bool, Dict]
+    error: str
+
+
+responses = {
+    200: {'model': Success},
+    400: {'model': Error},
+    422: {'model': Error, 'description': 'Validation Error'},
+    500: {'model': Error},
+}
diff --git a/src/services/api/rest/routers.py b/src/services/api/rest/routers.py
new file mode 100644
index 000000000..da981d5bf
--- /dev/null
+++ b/src/services/api/rest/routers.py
@@ -0,0 +1,754 @@
+# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library.  If not, see <http://www.gnu.org/licenses/>.
+
+
+# pylint: disable=line-too-long,raise-missing-from,invalid-name
+# pylint: disable=wildcard-import,unused-wildcard-import
+# pylint: disable=broad-exception-caught
+
+import json
+import copy
+import logging
+import traceback
+from threading import Lock
+from typing import Union
+from typing import Callable
+from typing import TYPE_CHECKING
+
+from fastapi import Depends
+from fastapi import Request
+from fastapi import Response
+from fastapi import HTTPException
+from fastapi import APIRouter
+from fastapi import BackgroundTasks
+from fastapi.routing import APIRoute
+from starlette.datastructures import FormData
+from starlette.formparsers import FormParser
+from starlette.formparsers import MultiPartParser
+from starlette.formparsers import MultiPartException
+from multipart.multipart import parse_options_header
+
+from vyos.config import Config
+from vyos.configtree import ConfigTree
+from vyos.configdiff import get_config_diff
+from vyos.configsession import ConfigSessionError
+
+from ..session import SessionState
+from .models import success
+from .models import error
+from .models import responses
+from .models import ApiModel
+from .models import ConfigureModel
+from .models import ConfigureListModel
+from .models import ConfigSectionModel
+from .models import ConfigSectionListModel
+from .models import ConfigSectionTreeModel
+from .models import BaseConfigSectionTreeModel
+from .models import BaseConfigureModel
+from .models import BaseConfigSectionModel
+from .models import RetrieveModel
+from .models import ConfigFileModel
+from .models import ImageModel
+from .models import ContainerImageModel
+from .models import GenerateModel
+from .models import ShowModel
+from .models import RebootModel
+from .models import ResetModel
+from .models import ImportPkiModel
+from .models import PoweroffModel
+
+
+if TYPE_CHECKING:
+    from fastapi import FastAPI
+
+
+LOG = logging.getLogger('http_api.routers')
+
+lock = Lock()
+
+
+def check_auth(key_list, key):
+    key_id = None
+    for k in key_list:
+        if k['key'] == key:
+            key_id = k['id']
+    return key_id
+
+
+def auth_required(data: ApiModel):
+    session = SessionState()
+    key = data.key
+    api_keys = session.keys
+    key_id = check_auth(api_keys, key)
+    if not key_id:
+        raise HTTPException(status_code=401, detail='Valid API key is required')
+    session.id = key_id
+
+
+# override Request and APIRoute classes in order to convert form request to json;
+# do all explicit validation here, for backwards compatability of error messages;
+# the explicit validation may be dropped, if desired, in favor of native
+# validation by FastAPI/Pydantic, as is used for application/json requests
+class MultipartRequest(Request):
+    """Override Request class to convert form request to json"""
+
+    # pylint: disable=attribute-defined-outside-init
+    # pylint: disable=too-many-branches,too-many-statements
+
+    _form_err = ()
+
+    @property
+    def form_err(self):
+        return self._form_err
+
+    @form_err.setter
+    def form_err(self, val):
+        if not self._form_err:
+            self._form_err = val
+
+    @property
+    def orig_headers(self):
+        self._orig_headers = super().headers
+        return self._orig_headers
+
+    @property
+    def headers(self):
+        self._headers = super().headers.mutablecopy()
+        self._headers['content-type'] = 'application/json'
+        return self._headers
+
+    async def _get_form(
+        self, *, max_files: int | float = 1000, max_fields: int | float = 1000
+    ) -> FormData:
+        if self._form is None:
+            assert (
+                parse_options_header is not None
+            ), 'The `python-multipart` library must be installed to use form parsing.'
+            content_type_header = self.orig_headers.get('Content-Type')
+            content_type: bytes
+            content_type, _ = parse_options_header(content_type_header)
+            if content_type == b'multipart/form-data':
+                try:
+                    multipart_parser = MultiPartParser(
+                        self.orig_headers,
+                        self.stream(),
+                        max_files=max_files,
+                        max_fields=max_fields,
+                    )
+                    self._form = await multipart_parser.parse()
+                except MultiPartException as exc:
+                    if 'app' in self.scope:
+                        raise HTTPException(status_code=400, detail=exc.message)
+                    raise exc
+            elif content_type == b'application/x-www-form-urlencoded':
+                form_parser = FormParser(self.orig_headers, self.stream())
+                self._form = await form_parser.parse()
+            else:
+                self._form = FormData()
+        return self._form
+
+    async def body(self) -> bytes:
+        if not hasattr(self, '_body'):
+            forms = {}
+            merge = {}
+            body = await super().body()
+            self._body = body
+
+            form_data = await self.form()
+            if form_data:
+                endpoint = self.url.path
+                LOG.debug('processing form data')
+                for k, v in form_data.multi_items():
+                    forms[k] = v
+
+                if 'data' not in forms:
+                    self.form_err = (422, 'Non-empty data field is required')
+                    return self._body
+                try:
+                    tmp = json.loads(forms['data'])
+                except json.JSONDecodeError as e:
+                    self.form_err = (400, f'Failed to parse JSON: {e}')
+                    return self._body
+                if isinstance(tmp, list):
+                    merge['commands'] = tmp
+                else:
+                    merge = tmp
+
+                if 'commands' in merge:
+                    cmds = merge['commands']
+                else:
+                    cmds = copy.deepcopy(merge)
+                    cmds = [cmds]
+
+                for c in cmds:
+                    if not isinstance(c, dict):
+                        self.form_err = (
+                            400,
+                            f"Malformed command '{c}': any command must be JSON of dict",
+                        )
+                        return self._body
+                    if 'op' not in c:
+                        self.form_err = (
+                            400,
+                            f"Malformed command '{c}': missing 'op' field",
+                        )
+                    if endpoint not in (
+                        '/config-file',
+                        '/container-image',
+                        '/image',
+                        '/configure-section',
+                    ):
+                        if 'path' not in c:
+                            self.form_err = (
+                                400,
+                                f"Malformed command '{c}': missing 'path' field",
+                            )
+                        elif not isinstance(c['path'], list):
+                            self.form_err = (
+                                400,
+                                f"Malformed command '{c}': 'path' field must be a list",
+                            )
+                        elif not all(isinstance(el, str) for el in c['path']):
+                            self.form_err = (
+                                400,
+                                f"Malformed command '{0}': 'path' field must be a list of strings",
+                            )
+                    if endpoint in ('/configure'):
+                        if not c['path']:
+                            self.form_err = (
+                                400,
+                                f"Malformed command '{c}': 'path' list must be non-empty",
+                            )
+                        if 'value' in c and not isinstance(c['value'], str):
+                            self.form_err = (
+                                400,
+                                f"Malformed command '{c}': 'value' field must be a string",
+                            )
+                    if endpoint in ('/configure-section'):
+                        if 'section' not in c and 'config' not in c:
+                            self.form_err = (
+                                400,
+                                f"Malformed command '{c}': missing 'section' or 'config' field",
+                            )
+
+                if 'key' not in forms and 'key' not in merge:
+                    self.form_err = (401, 'Valid API key is required')
+                if 'key' in forms and 'key' not in merge:
+                    merge['key'] = forms['key']
+
+                new_body = json.dumps(merge)
+                new_body = new_body.encode()
+                self._body = new_body
+
+        return self._body
+
+
+class MultipartRoute(APIRoute):
+    """Override APIRoute class to convert form request to json"""
+
+    def get_route_handler(self) -> Callable:
+        original_route_handler = super().get_route_handler()
+
+        async def custom_route_handler(request: Request) -> Response:
+            request = MultipartRequest(request.scope, request.receive)
+            try:
+                response: Response = await original_route_handler(request)
+            except HTTPException as e:
+                return error(e.status_code, e.detail)
+            except Exception as e:
+                form_err = request.form_err
+                if form_err:
+                    return error(*form_err)
+                raise e
+
+            return response
+
+        return custom_route_handler
+
+
+router = APIRouter(
+    route_class=MultipartRoute,
+    responses={**responses},
+    dependencies=[Depends(auth_required)],
+)
+
+
+self_ref_msg = 'Requested HTTP API server configuration change; commit will be called in the background'
+
+
+def call_commit(s: SessionState):
+    try:
+        s.session.commit()
+    except ConfigSessionError as e:
+        s.session.discard()
+        if s.debug:
+            LOG.warning(f'ConfigSessionError:\n {traceback.format_exc()}')
+        else:
+            LOG.warning(f'ConfigSessionError: {e}')
+
+
+def _configure_op(
+    data: Union[
+        ConfigureModel,
+        ConfigureListModel,
+        ConfigSectionModel,
+        ConfigSectionListModel,
+        ConfigSectionTreeModel,
+    ],
+    _request: Request,
+    background_tasks: BackgroundTasks,
+):
+    # pylint: disable=too-many-branches,too-many-locals,too-many-nested-blocks,too-many-statements
+    # pylint: disable=consider-using-with
+
+    state = SessionState()
+    session = state.session
+    env = session.get_session_env()
+
+    # Allow users to pass just one command
+    if not isinstance(data, (ConfigureListModel, ConfigSectionListModel)):
+        data = [data]
+    else:
+        data = data.commands
+
+    # We don't want multiple people/apps to be able to commit at once,
+    # or modify the shared session while someone else is doing the same,
+    # so the lock is really global
+    lock.acquire()
+
+    config = Config(session_env=env)
+
+    status = 200
+    msg = None
+    error_msg = None
+    try:
+        for c in data:
+            op = c.op
+            if not isinstance(c, BaseConfigSectionTreeModel):
+                path = c.path
+
+            if isinstance(c, BaseConfigureModel):
+                if c.value:
+                    value = c.value
+                else:
+                    value = ''
+                # For vyos.configsession calls that have no separate value arguments,
+                # and for type checking too
+                cfg_path = ' '.join(path + [value]).strip()
+
+            elif isinstance(c, BaseConfigSectionModel):
+                section = c.section
+
+            elif isinstance(c, BaseConfigSectionTreeModel):
+                mask = c.mask
+                config = c.config
+
+            if isinstance(c, BaseConfigureModel):
+                if op == 'set':
+                    session.set(path, value=value)
+                elif op == 'delete':
+                    if state.strict and not config.exists(cfg_path):
+                        raise ConfigSessionError(
+                            f'Cannot delete [{cfg_path}]: path/value does not exist'
+                        )
+                    session.delete(path, value=value)
+                elif op == 'comment':
+                    session.comment(path, value=value)
+                else:
+                    raise ConfigSessionError(f"'{op}' is not a valid operation")
+
+            elif isinstance(c, BaseConfigSectionModel):
+                if op == 'set':
+                    session.set_section(path, section)
+                elif op == 'load':
+                    session.load_section(path, section)
+                else:
+                    raise ConfigSessionError(f"'{op}' is not a valid operation")
+
+            elif isinstance(c, BaseConfigSectionTreeModel):
+                if op == 'set':
+                    session.set_section_tree(config)
+                elif op == 'load':
+                    session.load_section_tree(mask, config)
+                else:
+                    raise ConfigSessionError(f"'{op}' is not a valid operation")
+        # end for
+        config = Config(session_env=env)
+        d = get_config_diff(config)
+
+        if d.is_node_changed(['service', 'https']):
+            background_tasks.add_task(call_commit, state)
+            msg = self_ref_msg
+        else:
+            # capture non-fatal warnings
+            out = session.commit()
+            msg = out if out else msg
+
+        LOG.info(f"Configuration modified via HTTP API using key '{state.id}'")
+    except ConfigSessionError as e:
+        session.discard()
+        status = 400
+        if state.debug:
+            LOG.critical(f'ConfigSessionError:\n {traceback.format_exc()}')
+        error_msg = str(e)
+    except Exception:
+        session.discard()
+        LOG.critical(traceback.format_exc())
+        status = 500
+
+        # Don't give the details away to the outer world
+        error_msg = 'An internal error occured. Check the logs for details.'
+    finally:
+        lock.release()
+
+    if status != 200:
+        return error(status, error_msg)
+
+    return success(msg)
+
+
+def create_path_import_pki_no_prompt(path):
+    correct_paths = ['ca', 'certificate', 'key-pair']
+    if path[1] not in correct_paths:
+        return False
+    path[1] = '--' + path[1].replace('-', '')
+    path[3] = '--key-filename'
+    return path[1:]
+
+
+@router.post('/configure')
+def configure_op(
+    data: Union[ConfigureModel, ConfigureListModel],
+    request: Request,
+    background_tasks: BackgroundTasks,
+):
+    return _configure_op(data, request, background_tasks)
+
+
+@router.post('/configure-section')
+def configure_section_op(
+    data: Union[ConfigSectionModel, ConfigSectionListModel, ConfigSectionTreeModel],
+    request: Request,
+    background_tasks: BackgroundTasks,
+):
+    return _configure_op(data, request, background_tasks)
+
+
+@router.post('/retrieve')
+async def retrieve_op(data: RetrieveModel):
+    state = SessionState()
+    session = state.session
+    env = session.get_session_env()
+    config = Config(session_env=env)
+
+    op = data.op
+    path = ' '.join(data.path)
+
+    try:
+        if op == 'returnValue':
+            res = config.return_value(path)
+        elif op == 'returnValues':
+            res = config.return_values(path)
+        elif op == 'exists':
+            res = config.exists(path)
+        elif op == 'showConfig':
+            config_format = 'json'
+            if data.configFormat:
+                config_format = data.configFormat
+
+            res = session.show_config(path=data.path)
+            if config_format == 'json':
+                config_tree = ConfigTree(res)
+                res = json.loads(config_tree.to_json())
+            elif config_format == 'json_ast':
+                config_tree = ConfigTree(res)
+                res = json.loads(config_tree.to_json_ast())
+            elif config_format == 'raw':
+                pass
+            else:
+                return error(400, f"'{config_format}' is not a valid config format")
+        else:
+            return error(400, f"'{op}' is not a valid operation")
+    except ConfigSessionError as e:
+        return error(400, str(e))
+    except Exception:
+        LOG.critical(traceback.format_exc())
+        return error(500, 'An internal error occured. Check the logs for details.')
+
+    return success(res)
+
+
+@router.post('/config-file')
+def config_file_op(data: ConfigFileModel, background_tasks: BackgroundTasks):
+    state = SessionState()
+    session = state.session
+    env = session.get_session_env()
+    op = data.op
+    msg = None
+
+    try:
+        if op == 'save':
+            if data.file:
+                path = data.file
+            else:
+                path = '/config/config.boot'
+            msg = session.save_config(path)
+        elif op == 'load':
+            if data.file:
+                path = data.file
+            else:
+                return error(400, 'Missing required field "file"')
+
+            session.migrate_and_load_config(path)
+
+            config = Config(session_env=env)
+            d = get_config_diff(config)
+
+            if d.is_node_changed(['service', 'https']):
+                background_tasks.add_task(call_commit, state)
+                msg = self_ref_msg
+            else:
+                session.commit()
+        else:
+            return error(400, f"'{op}' is not a valid operation")
+    except ConfigSessionError as e:
+        return error(400, str(e))
+    except Exception:
+        LOG.critical(traceback.format_exc())
+        return error(500, 'An internal error occured. Check the logs for details.')
+
+    return success(msg)
+
+
+@router.post('/image')
+def image_op(data: ImageModel):
+    state = SessionState()
+    session = state.session
+
+    op = data.op
+
+    try:
+        if op == 'add':
+            res = session.install_image(data.url)
+        elif op == 'delete':
+            res = session.remove_image(data.name)
+        elif op == 'show':
+            res = session.show(['system', 'image'])
+        elif op == 'set_default':
+            res = session.set_default_image(data.name)
+    except ConfigSessionError as e:
+        return error(400, str(e))
+    except Exception:
+        LOG.critical(traceback.format_exc())
+        return error(500, 'An internal error occured. Check the logs for details.')
+
+    return success(res)
+
+
+@router.post('/container-image')
+def container_image_op(data: ContainerImageModel):
+    state = SessionState()
+    session = state.session
+
+    op = data.op
+
+    try:
+        if op == 'add':
+            if data.name:
+                name = data.name
+            else:
+                return error(400, 'Missing required field "name"')
+            res = session.add_container_image(name)
+        elif op == 'delete':
+            if data.name:
+                name = data.name
+            else:
+                return error(400, 'Missing required field "name"')
+            res = session.delete_container_image(name)
+        elif op == 'show':
+            res = session.show_container_image()
+        else:
+            return error(400, f"'{op}' is not a valid operation")
+    except ConfigSessionError as e:
+        return error(400, str(e))
+    except Exception:
+        LOG.critical(traceback.format_exc())
+        return error(500, 'An internal error occured. Check the logs for details.')
+
+    return success(res)
+
+
+@router.post('/generate')
+def generate_op(data: GenerateModel):
+    state = SessionState()
+    session = state.session
+
+    op = data.op
+    path = data.path
+
+    try:
+        if op == 'generate':
+            res = session.generate(path)
+        else:
+            return error(400, f"'{op}' is not a valid operation")
+    except ConfigSessionError as e:
+        return error(400, str(e))
+    except Exception:
+        LOG.critical(traceback.format_exc())
+        return error(500, 'An internal error occured. Check the logs for details.')
+
+    return success(res)
+
+
+@router.post('/show')
+def show_op(data: ShowModel):
+    state = SessionState()
+    session = state.session
+
+    op = data.op
+    path = data.path
+
+    try:
+        if op == 'show':
+            res = session.show(path)
+        else:
+            return error(400, f"'{op}' is not a valid operation")
+    except ConfigSessionError as e:
+        return error(400, str(e))
+    except Exception:
+        LOG.critical(traceback.format_exc())
+        return error(500, 'An internal error occured. Check the logs for details.')
+
+    return success(res)
+
+
+@router.post('/reboot')
+def reboot_op(data: RebootModel):
+    state = SessionState()
+    session = state.session
+
+    op = data.op
+    path = data.path
+
+    try:
+        if op == 'reboot':
+            res = session.reboot(path)
+        else:
+            return error(400, f"'{op}' is not a valid operation")
+    except ConfigSessionError as e:
+        return error(400, str(e))
+    except Exception:
+        LOG.critical(traceback.format_exc())
+        return error(500, 'An internal error occured. Check the logs for details.')
+
+    return success(res)
+
+
+@router.post('/reset')
+def reset_op(data: ResetModel):
+    state = SessionState()
+    session = state.session
+
+    op = data.op
+    path = data.path
+
+    try:
+        if op == 'reset':
+            res = session.reset(path)
+        else:
+            return error(400, f"'{op}' is not a valid operation")
+    except ConfigSessionError as e:
+        return error(400, str(e))
+    except Exception:
+        LOG.critical(traceback.format_exc())
+        return error(500, 'An internal error occured. Check the logs for details.')
+
+    return success(res)
+
+
+@router.post('/import-pki')
+def import_pki(data: ImportPkiModel):
+    # pylint: disable=consider-using-with
+
+    state = SessionState()
+    session = state.session
+
+    op = data.op
+    path = data.path
+
+    lock.acquire()
+
+    try:
+        if op == 'import-pki':
+            # need to get rid or interactive mode for private key
+            if len(path) == 5 and path[3] in ['key-file', 'private-key']:
+                path_no_prompt = create_path_import_pki_no_prompt(path)
+                if not path_no_prompt:
+                    return error(400, f"Invalid command: {' '.join(path)}")
+                if data.passphrase:
+                    path_no_prompt += ['--passphrase', data.passphrase]
+                res = session.import_pki_no_prompt(path_no_prompt)
+            else:
+                res = session.import_pki(path)
+            if not res[0].isdigit():
+                return error(400, res)
+            # commit changes
+            session.commit()
+            res = res.split('. ')[0]
+        else:
+            return error(400, f"'{op}' is not a valid operation")
+    except ConfigSessionError as e:
+        return error(400, str(e))
+    except Exception:
+        LOG.critical(traceback.format_exc())
+        return error(500, 'An internal error occured. Check the logs for details.')
+    finally:
+        lock.release()
+
+    return success(res)
+
+
+@router.post('/poweroff')
+def poweroff_op(data: PoweroffModel):
+    state = SessionState()
+    session = state.session
+
+    op = data.op
+    path = data.path
+
+    try:
+        if op == 'poweroff':
+            res = session.poweroff(path)
+        else:
+            return error(400, f"'{op}' is not a valid operation")
+    except ConfigSessionError as e:
+        return error(400, str(e))
+    except Exception:
+        LOG.critical(traceback.format_exc())
+        return error(500, 'An internal error occured. Check the logs for details.')
+
+    return success(res)
+
+
+def rest_init(app: 'FastAPI'):
+    if all(r in app.routes for r in router.routes):
+        return
+    app.include_router(router)
+
+
+def rest_clear(app: 'FastAPI'):
+    for r in router.routes:
+        if r in app.routes:
+            app.routes.remove(r)
diff --git a/src/services/api/session.py b/src/services/api/session.py
new file mode 100644
index 000000000..ad3ef660c
--- /dev/null
+++ b/src/services/api/session.py
@@ -0,0 +1,41 @@
+# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library.  If not, see <http://www.gnu.org/licenses/>.
+
+
+class SessionState:
+    # pylint: disable=attribute-defined-outside-init
+    # pylint: disable=too-many-instance-attributes,too-few-public-methods
+
+    _instance = None
+
+    def __new__(cls):
+        if cls._instance is None:
+            cls._instance = super(SessionState, cls).__new__(cls)
+            cls._instance._initialize()
+        return cls._instance
+
+    def _initialize(self):
+        self.session = None
+        self.keys = []
+        self.id = None
+        self.rest = False
+        self.debug = False
+        self.strict = False
+        self.graphql = False
+        self.origins = []
+        self.introspection = False
+        self.auth_type = None
+        self.token_exp = None
+        self.secret_len = None
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index 91100410c..558561182 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -1,1036 +1,220 @@
 #!/usr/share/vyos-http-api-tools/bin/python3
 #
 # Copyright (C) 2019-2024 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
 import sys
 import grp
-import copy
 import json
 import logging
 import signal
-import traceback
-import threading
-from enum import Enum
-
 from time import sleep
-from typing import List, Union, Callable, Dict, Self
 
-from fastapi import FastAPI, Depends, Request, Response, HTTPException
-from fastapi import BackgroundTasks
-from fastapi.responses import HTMLResponse
+from fastapi import FastAPI
 from fastapi.exceptions import RequestValidationError
-from fastapi.routing import APIRoute
-from pydantic import BaseModel, StrictStr, validator, model_validator
-from starlette.middleware.cors import CORSMiddleware
-from starlette.datastructures import FormData
-from starlette.formparsers import FormParser, MultiPartParser
-from multipart.multipart import parse_options_header
 from uvicorn import Config as UvicornConfig
 from uvicorn import Server as UvicornServer
 
-from ariadne.asgi import GraphQL
-
-from vyos.config import Config
-from vyos.configtree import ConfigTree
-from vyos.configdiff import get_config_diff
 from vyos.configsession import ConfigSession
-from vyos.configsession import ConfigSessionError
 from vyos.defaults import api_config_state
 
-import api.graphql.state
+from api.session import SessionState
+from api.rest.models import error
 
 CFG_GROUP = 'vyattacfg'
 
 debug = True
 
-logger = logging.getLogger(__name__)
+LOG = logging.getLogger('http_api')
 logs_handler = logging.StreamHandler()
-logger.addHandler(logs_handler)
+LOG.addHandler(logs_handler)
 
 if debug:
-    logger.setLevel(logging.DEBUG)
+    LOG.setLevel(logging.DEBUG)
 else:
-    logger.setLevel(logging.INFO)
+    LOG.setLevel(logging.INFO)
 
-# Giant lock!
-lock = threading.Lock()
 
 def load_server_config():
     with open(api_config_state) as f:
         config = json.load(f)
     return config
 
-def check_auth(key_list, key):
-    key_id = None
-    for k in key_list:
-        if k['key'] == key:
-            key_id = k['id']
-    return key_id
-
-def error(code, msg):
-    resp = {"success": False, "error": msg, "data": None}
-    resp = json.dumps(resp)
-    return HTMLResponse(resp, status_code=code)
-
-def success(data):
-    resp = {"success": True, "data": data, "error": None}
-    resp = json.dumps(resp)
-    return HTMLResponse(resp)
-
-# Pydantic models for validation
-# Pydantic will cast when possible, so use StrictStr
-# validators added as needed for additional constraints
-# schema_extra adds anotations to OpenAPI, to add examples
-
-class ApiModel(BaseModel):
-    key: StrictStr
-
-class BasePathModel(BaseModel):
-    op: StrictStr
-    path: List[StrictStr]
-
-    @validator("path")
-    def check_non_empty(cls, path):
-        if not len(path) > 0:
-            raise ValueError('path must be non-empty')
-        return path
-
-class BaseConfigureModel(BasePathModel):
-    value: StrictStr = None
-
-class ConfigureModel(ApiModel, BaseConfigureModel):
-    class Config:
-        schema_extra = {
-            "example": {
-                "key": "id_key",
-                "op": "set | delete | comment",
-                "path": ['config', 'mode', 'path'],
-            }
-        }
-
-class ConfigureListModel(ApiModel):
-    commands: List[BaseConfigureModel]
-
-    class Config:
-        schema_extra = {
-            "example": {
-                "key": "id_key",
-                "commands": "list of commands",
-            }
-        }
-
-class BaseConfigSectionModel(BasePathModel):
-    section: Dict
-
-class ConfigSectionModel(ApiModel, BaseConfigSectionModel):
-    pass
-
-class ConfigSectionListModel(ApiModel):
-    commands: List[BaseConfigSectionModel]
-
-class BaseConfigSectionTreeModel(BaseModel):
-    op: StrictStr
-    mask: Dict
-    config: Dict
-
-class ConfigSectionTreeModel(ApiModel, BaseConfigSectionTreeModel):
-    pass
-
-class RetrieveModel(ApiModel):
-    op: StrictStr
-    path: List[StrictStr]
-    configFormat: StrictStr = None
-
-    class Config:
-        schema_extra = {
-            "example": {
-                "key": "id_key",
-                "op": "returnValue | returnValues | exists | showConfig",
-                "path": ['config', 'mode', 'path'],
-                "configFormat": "json (default) | json_ast | raw",
-
-            }
-        }
-
-class ConfigFileModel(ApiModel):
-    op: StrictStr
-    file: StrictStr = None
-
-    class Config:
-        schema_extra = {
-            "example": {
-                "key": "id_key",
-                "op": "save | load",
-                "file": "filename",
-            }
-        }
-
-
-class ImageOp(str, Enum):
-    add = "add"
-    delete = "delete"
-    show = "show"
-    set_default = "set_default"
-
-
-class ImageModel(ApiModel):
-    op: ImageOp
-    url: StrictStr = None
-    name: StrictStr = None
-
-    @model_validator(mode='after')
-    def check_data(self) -> Self:
-        if self.op == 'add':
-            if not self.url:
-                raise ValueError("Missing required field \"url\"")
-        elif self.op in ['delete', 'set_default']:
-            if not self.name:
-                raise ValueError("Missing required field \"name\"")
-
-        return self
-
-    class Config:
-        schema_extra = {
-            "example": {
-                "key": "id_key",
-                "op": "add | delete | show | set_default",
-                "url": "imagelocation",
-                "name": "imagename",
-            }
-        }
-
-class ImportPkiModel(ApiModel):
-    op: StrictStr
-    path: List[StrictStr]
-    passphrase: StrictStr = None
-
-    class Config:
-        schema_extra = {
-            "example": {
-                "key": "id_key",
-                "op": "import_pki",
-                "path": ["op", "mode", "path"],
-                "passphrase": "passphrase",
-            }
-        }
-
-
-class ContainerImageModel(ApiModel):
-    op: StrictStr
-    name: StrictStr = None
-
-    class Config:
-        schema_extra = {
-            "example": {
-                "key": "id_key",
-                "op": "add | delete | show",
-                "name": "imagename",
-            }
-        }
-
-class GenerateModel(ApiModel):
-    op: StrictStr
-    path: List[StrictStr]
-
-    class Config:
-        schema_extra = {
-            "example": {
-                "key": "id_key",
-                "op": "generate",
-                "path": ["op", "mode", "path"],
-            }
-        }
-
-class ShowModel(ApiModel):
-    op: StrictStr
-    path: List[StrictStr]
-
-    class Config:
-        schema_extra = {
-            "example": {
-                "key": "id_key",
-                "op": "show",
-                "path": ["op", "mode", "path"],
-            }
-        }
-
-class RebootModel(ApiModel):
-    op: StrictStr
-    path: List[StrictStr]
-
-    class Config:
-        schema_extra = {
-            "example": {
-                "key": "id_key",
-                "op": "reboot",
-                "path": ["op", "mode", "path"],
-            }
-        }
-
-class ResetModel(ApiModel):
-    op: StrictStr
-    path: List[StrictStr]
-
-    class Config:
-        schema_extra = {
-            "example": {
-                "key": "id_key",
-                "op": "reset",
-                "path": ["op", "mode", "path"],
-            }
-        }
-
-class PoweroffModel(ApiModel):
-    op: StrictStr
-    path: List[StrictStr]
-
-    class Config:
-        schema_extra = {
-            "example": {
-                "key": "id_key",
-                "op": "poweroff",
-                "path": ["op", "mode", "path"],
-            }
-        }
-
-
-class Success(BaseModel):
-    success: bool
-    data: Union[str, bool, Dict]
-    error: str
-
-class Error(BaseModel):
-    success: bool = False
-    data: Union[str, bool, Dict]
-    error: str
-
-responses = {
-    200: {'model': Success},
-    400: {'model': Error},
-    422: {'model': Error, 'description': 'Validation Error'},
-    500: {'model': Error}
-}
-
-def auth_required(data: ApiModel):
-    key = data.key
-    api_keys = app.state.vyos_keys
-    key_id = check_auth(api_keys, key)
-    if not key_id:
-        raise HTTPException(status_code=401, detail="Valid API key is required")
-    app.state.vyos_id = key_id
-
-# override Request and APIRoute classes in order to convert form request to json;
-# do all explicit validation here, for backwards compatability of error messages;
-# the explicit validation may be dropped, if desired, in favor of native
-# validation by FastAPI/Pydantic, as is used for application/json requests
-class MultipartRequest(Request):
-    _form_err = ()
-    @property
-    def form_err(self):
-        return self._form_err
-
-    @form_err.setter
-    def form_err(self, val):
-        if not self._form_err:
-            self._form_err = val
-
-    @property
-    def orig_headers(self):
-        self._orig_headers = super().headers
-        return self._orig_headers
-
-    @property
-    def headers(self):
-        self._headers = super().headers.mutablecopy()
-        self._headers['content-type'] = 'application/json'
-        return self._headers
-
-    async def form(self) -> FormData:
-        if self._form is None:
-            assert (
-                parse_options_header is not None
-            ), "The `python-multipart` library must be installed to use form parsing."
-            content_type_header = self.orig_headers.get("Content-Type")
-            content_type, options = parse_options_header(content_type_header)
-            if content_type == b"multipart/form-data":
-                multipart_parser = MultiPartParser(self.orig_headers, self.stream())
-                self._form = await multipart_parser.parse()
-            elif content_type == b"application/x-www-form-urlencoded":
-                form_parser = FormParser(self.orig_headers, self.stream())
-                self._form = await form_parser.parse()
-            else:
-                self._form = FormData()
-        return self._form
-
-    async def body(self) -> bytes:
-        if not hasattr(self, "_body"):
-            forms = {}
-            merge = {}
-            body = await super().body()
-            self._body = body
-
-            form_data = await self.form()
-            if form_data:
-                endpoint = self.url.path
-                logger.debug("processing form data")
-                for k, v in form_data.multi_items():
-                    forms[k] = v
-
-                if 'data' not in forms:
-                    self.form_err = (422, "Non-empty data field is required")
-                    return self._body
-                else:
-                    try:
-                        tmp = json.loads(forms['data'])
-                    except json.JSONDecodeError as e:
-                        self.form_err = (400, f'Failed to parse JSON: {e}')
-                        return self._body
-                    if isinstance(tmp, list):
-                        merge['commands'] = tmp
-                    else:
-                        merge = tmp
-
-                if 'commands' in merge:
-                    cmds = merge['commands']
-                else:
-                    cmds = copy.deepcopy(merge)
-                    cmds = [cmds]
-
-                for c in cmds:
-                    if not isinstance(c, dict):
-                        self.form_err = (400,
-                        f"Malformed command '{c}': any command must be JSON of dict")
-                        return self._body
-                    if 'op' not in c:
-                        self.form_err = (400,
-                        f"Malformed command '{c}': missing 'op' field")
-                    if endpoint not in ('/config-file', '/container-image',
-                                        '/image', '/configure-section'):
-                        if 'path' not in c:
-                            self.form_err = (400,
-                            f"Malformed command '{c}': missing 'path' field")
-                        elif not isinstance(c['path'], list):
-                            self.form_err = (400,
-                            f"Malformed command '{c}': 'path' field must be a list")
-                        elif not all(isinstance(el, str) for el in c['path']):
-                            self.form_err = (400,
-                            f"Malformed command '{0}': 'path' field must be a list of strings")
-                    if endpoint in ('/configure'):
-                        if not c['path']:
-                            self.form_err = (400,
-                            f"Malformed command '{c}': 'path' list must be non-empty")
-                        if 'value' in c and not isinstance(c['value'], str):
-                            self.form_err = (400,
-                            f"Malformed command '{c}': 'value' field must be a string")
-                    if endpoint in ('/configure-section'):
-                        if 'section' not in c and 'config' not in c:
-                            self.form_err = (400,
-                            f"Malformed command '{c}': missing 'section' or 'config' field")
-
-                if 'key' not in forms and 'key' not in merge:
-                    self.form_err = (401, "Valid API key is required")
-                if 'key' in forms and 'key' not in merge:
-                    merge['key'] = forms['key']
-
-                new_body = json.dumps(merge)
-                new_body = new_body.encode()
-                self._body = new_body
-
-        return self._body
-
-class MultipartRoute(APIRoute):
-    def get_route_handler(self) -> Callable:
-        original_route_handler = super().get_route_handler()
-
-        async def custom_route_handler(request: Request) -> Response:
-            request = MultipartRequest(request.scope, request.receive)
-            try:
-                response: Response = await original_route_handler(request)
-            except HTTPException as e:
-                return error(e.status_code, e.detail)
-            except Exception as e:
-                form_err = request.form_err
-                if form_err:
-                    return error(*form_err)
-                raise e
-
-            return response
-
-        return custom_route_handler
 
 app = FastAPI(debug=True,
               title="VyOS API",
-              version="0.1.0",
-              responses={**responses},
-              dependencies=[Depends(auth_required)])
-
-app.router.route_class = MultipartRoute
+              version="0.1.0")
 
 @app.exception_handler(RequestValidationError)
-async def validation_exception_handler(request, exc):
+async def validation_exception_handler(_request, exc):
     return error(400, str(exc.errors()[0]))
 
-self_ref_msg = "Requested HTTP API server configuration change; commit will be called in the background"
-
-def call_commit(s: ConfigSession):
-    try:
-        s.commit()
-    except ConfigSessionError as e:
-        s.discard()
-        if app.state.vyos_debug:
-            logger.warning(f"ConfigSessionError:\n {traceback.format_exc()}")
-        else:
-            logger.warning(f"ConfigSessionError: {e}")
-
-def _configure_op(data: Union[ConfigureModel, ConfigureListModel,
-                              ConfigSectionModel, ConfigSectionListModel,
-                              ConfigSectionTreeModel],
-                  request: Request, background_tasks: BackgroundTasks):
-    session = app.state.vyos_session
-    env = session.get_session_env()
-
-    endpoint = request.url.path
-
-    # Allow users to pass just one command
-    if not isinstance(data, (ConfigureListModel, ConfigSectionListModel)):
-        data = [data]
-    else:
-        data = data.commands
-
-    # We don't want multiple people/apps to be able to commit at once,
-    # or modify the shared session while someone else is doing the same,
-    # so the lock is really global
-    lock.acquire()
-
-    config = Config(session_env=env)
-
-    status = 200
-    msg = None
-    error_msg = None
-    try:
-        for c in data:
-            op = c.op
-            if not isinstance(c, BaseConfigSectionTreeModel):
-                path = c.path
-
-            if isinstance(c, BaseConfigureModel):
-                if c.value:
-                    value = c.value
-                else:
-                    value = ""
-                # For vyos.configsession calls that have no separate value arguments,
-                # and for type checking too
-                cfg_path = " ".join(path + [value]).strip()
-
-            elif isinstance(c, BaseConfigSectionModel):
-                section = c.section
-
-            elif isinstance(c, BaseConfigSectionTreeModel):
-                mask = c.mask
-                config = c.config
-
-            if isinstance(c, BaseConfigureModel):
-                if op == 'set':
-                    session.set(path, value=value)
-                elif op == 'delete':
-                    if app.state.vyos_strict and not config.exists(cfg_path):
-                        raise ConfigSessionError(f"Cannot delete [{cfg_path}]: path/value does not exist")
-                    session.delete(path, value=value)
-                elif op == 'comment':
-                    session.comment(path, value=value)
-                else:
-                    raise ConfigSessionError(f"'{op}' is not a valid operation")
-
-            elif isinstance(c, BaseConfigSectionModel):
-                if op == 'set':
-                    session.set_section(path, section)
-                elif op == 'load':
-                    session.load_section(path, section)
-                else:
-                    raise ConfigSessionError(f"'{op}' is not a valid operation")
-
-            elif isinstance(c, BaseConfigSectionTreeModel):
-                if op == 'set':
-                    session.set_section_tree(config)
-                elif op == 'load':
-                    session.load_section_tree(mask, config)
-                else:
-                    raise ConfigSessionError(f"'{op}' is not a valid operation")
-        # end for
-        config = Config(session_env=env)
-        d = get_config_diff(config)
-
-        if d.is_node_changed(['service', 'https']):
-            background_tasks.add_task(call_commit, session)
-            msg = self_ref_msg
-        else:
-            # capture non-fatal warnings
-            out = session.commit()
-            msg = out if out else msg
-
-        logger.info(f"Configuration modified via HTTP API using key '{app.state.vyos_id}'")
-    except ConfigSessionError as e:
-        session.discard()
-        status = 400
-        if app.state.vyos_debug:
-            logger.critical(f"ConfigSessionError:\n {traceback.format_exc()}")
-        error_msg = str(e)
-    except Exception as e:
-        session.discard()
-        logger.critical(traceback.format_exc())
-        status = 500
-
-        # Don't give the details away to the outer world
-        error_msg = "An internal error occured. Check the logs for details."
-    finally:
-        lock.release()
-
-    if status != 200:
-        return error(status, error_msg)
-
-    return success(msg)
-
-def create_path_import_pki_no_prompt(path):
-    correct_paths = ['ca', 'certificate', 'key-pair']
-    if path[1] not in correct_paths:
-        return False
-    path[1] = '--' + path[1].replace('-', '')
-    path[3] = '--key-filename'
-    return path[1:]
-
-@app.post('/configure')
-def configure_op(data: Union[ConfigureModel,
-                             ConfigureListModel],
-                       request: Request, background_tasks: BackgroundTasks):
-    return _configure_op(data, request, background_tasks)
-
-@app.post('/configure-section')
-def configure_section_op(data: Union[ConfigSectionModel,
-                                     ConfigSectionListModel,
-                                     ConfigSectionTreeModel],
-                               request: Request, background_tasks: BackgroundTasks):
-    return _configure_op(data, request, background_tasks)
-
-@app.post("/retrieve")
-async def retrieve_op(data: RetrieveModel):
-    session = app.state.vyos_session
-    env = session.get_session_env()
-    config = Config(session_env=env)
-
-    op = data.op
-    path = " ".join(data.path)
-
-    try:
-        if op == 'returnValue':
-            res = config.return_value(path)
-        elif op == 'returnValues':
-            res = config.return_values(path)
-        elif op == 'exists':
-            res = config.exists(path)
-        elif op == 'showConfig':
-            config_format = 'json'
-            if data.configFormat:
-                config_format = data.configFormat
-
-            res = session.show_config(path=data.path)
-            if config_format == 'json':
-                config_tree = ConfigTree(res)
-                res = json.loads(config_tree.to_json())
-            elif config_format == 'json_ast':
-                config_tree = ConfigTree(res)
-                res = json.loads(config_tree.to_json_ast())
-            elif config_format == 'raw':
-                pass
-            else:
-                return error(400, f"'{config_format}' is not a valid config format")
-        else:
-            return error(400, f"'{op}' is not a valid operation")
-    except ConfigSessionError as e:
-        return error(400, str(e))
-    except Exception as e:
-        logger.critical(traceback.format_exc())
-        return error(500, "An internal error occured. Check the logs for details.")
-
-    return success(res)
-
-@app.post('/config-file')
-def config_file_op(data: ConfigFileModel, background_tasks: BackgroundTasks):
-    session = app.state.vyos_session
-    env = session.get_session_env()
-    op = data.op
-    msg = None
-
-    try:
-        if op == 'save':
-            if data.file:
-                path = data.file
-            else:
-                path = '/config/config.boot'
-            msg = session.save_config(path)
-        elif op == 'load':
-            if data.file:
-                path = data.file
-            else:
-                return error(400, "Missing required field \"file\"")
-
-            session.migrate_and_load_config(path)
-
-            config = Config(session_env=env)
-            d = get_config_diff(config)
-
-            if d.is_node_changed(['service', 'https']):
-                background_tasks.add_task(call_commit, session)
-                msg = self_ref_msg
-            else:
-                session.commit()
-        else:
-            return error(400, f"'{op}' is not a valid operation")
-    except ConfigSessionError as e:
-        return error(400, str(e))
-    except Exception as e:
-        logger.critical(traceback.format_exc())
-        return error(500, "An internal error occured. Check the logs for details.")
-
-    return success(msg)
-
-@app.post('/image')
-def image_op(data: ImageModel):
-    session = app.state.vyos_session
-
-    op = data.op
-
-    try:
-        if op == 'add':
-            res = session.install_image(data.url)
-        elif op == 'delete':
-            res = session.remove_image(data.name)
-        elif op == 'show':
-            res = session.show(["system", "image"])
-        elif op == 'set_default':
-            res = session.set_default_image(data.name)
-    except ConfigSessionError as e:
-        return error(400, str(e))
-    except Exception as e:
-        logger.critical(traceback.format_exc())
-        return error(500, "An internal error occured. Check the logs for details.")
-
-    return success(res)
-
-@app.post('/container-image')
-def container_image_op(data: ContainerImageModel):
-    session = app.state.vyos_session
-
-    op = data.op
-
-    try:
-        if op == 'add':
-            if data.name:
-                name = data.name
-            else:
-                return error(400, "Missing required field \"name\"")
-            res = session.add_container_image(name)
-        elif op == 'delete':
-            if data.name:
-                name = data.name
-            else:
-                return error(400, "Missing required field \"name\"")
-            res = session.delete_container_image(name)
-        elif op == 'show':
-            res = session.show_container_image()
-        else:
-            return error(400, f"'{op}' is not a valid operation")
-    except ConfigSessionError as e:
-        return error(400, str(e))
-    except Exception as e:
-        logger.critical(traceback.format_exc())
-        return error(500, "An internal error occured. Check the logs for details.")
-
-    return success(res)
-
-@app.post('/generate')
-def generate_op(data: GenerateModel):
-    session = app.state.vyos_session
-
-    op = data.op
-    path = data.path
-
-    try:
-        if op == 'generate':
-            res = session.generate(path)
-        else:
-            return error(400, f"'{op}' is not a valid operation")
-    except ConfigSessionError as e:
-        return error(400, str(e))
-    except Exception as e:
-        logger.critical(traceback.format_exc())
-        return error(500, "An internal error occured. Check the logs for details.")
-
-    return success(res)
-
-@app.post('/show')
-def show_op(data: ShowModel):
-    session = app.state.vyos_session
-
-    op = data.op
-    path = data.path
-
-    try:
-        if op == 'show':
-            res = session.show(path)
-        else:
-            return error(400, f"'{op}' is not a valid operation")
-    except ConfigSessionError as e:
-        return error(400, str(e))
-    except Exception as e:
-        logger.critical(traceback.format_exc())
-        return error(500, "An internal error occured. Check the logs for details.")
-
-    return success(res)
-
-@app.post('/reboot')
-def reboot_op(data: RebootModel):
-    session = app.state.vyos_session
-
-    op = data.op
-    path = data.path
-
-    try:
-        if op == 'reboot':
-            res = session.reboot(path)
-        else:
-            return error(400, f"'{op}' is not a valid operation")
-    except ConfigSessionError as e:
-        return error(400, str(e))
-    except Exception as e:
-        logger.critical(traceback.format_exc())
-        return error(500, "An internal error occured. Check the logs for details.")
-
-    return success(res)
-
-@app.post('/reset')
-def reset_op(data: ResetModel):
-    session = app.state.vyos_session
-
-    op = data.op
-    path = data.path
-
-    try:
-        if op == 'reset':
-            res = session.reset(path)
-        else:
-            return error(400, f"'{op}' is not a valid operation")
-    except ConfigSessionError as e:
-        return error(400, str(e))
-    except Exception as e:
-        logger.critical(traceback.format_exc())
-        return error(500, "An internal error occured. Check the logs for details.")
-
-    return success(res)
-
-@app.post('/import-pki')
-def import_pki(data: ImportPkiModel):
-    session = app.state.vyos_session
-
-    op = data.op
-    path = data.path
 
-    lock.acquire()
-
-    try:
-        if op == 'import-pki':
-            # need to get rid or interactive mode for private key
-            if len(path) == 5 and path[3] in ['key-file', 'private-key']:
-                path_no_prompt = create_path_import_pki_no_prompt(path)
-                if not path_no_prompt:
-                    return error(400, f"Invalid command: {' '.join(path)}")
-                if data.passphrase:
-                    path_no_prompt += ['--passphrase', data.passphrase]
-                res = session.import_pki_no_prompt(path_no_prompt)
-            else:
-                res = session.import_pki(path)
-            if not res[0].isdigit():
-                return error(400, res)
-            # commit changes
-            session.commit()
-            res = res.split('. ')[0]
-        else:
-            return error(400, f"'{op}' is not a valid operation")
-    except ConfigSessionError as e:
-        return error(400, str(e))
-    except Exception as e:
-        logger.critical(traceback.format_exc())
-        return error(500, "An internal error occured. Check the logs for details.")
-    finally:
-        lock.release()
-
-    return success(res)
-
-@app.post('/poweroff')
-def poweroff_op(data: PoweroffModel):
-    session = app.state.vyos_session
-
-    op = data.op
-    path = data.path
-
-    try:
-        if op == 'poweroff':
-            res = session.poweroff(path)
-        else:
-            return error(400, f"'{op}' is not a valid operation")
-    except ConfigSessionError as e:
-        return error(400, str(e))
-    except Exception as e:
-        logger.critical(traceback.format_exc())
-        return error(500, "An internal error occured. Check the logs for details.")
-
-    return success(res)
-
-
-###
-# GraphQL integration
-###
-
-def graphql_init(app: FastAPI = app):
-    from api.graphql.libs.token_auth import get_user_context
-    api.graphql.state.init()
-    api.graphql.state.settings['app'] = app
-
-    # import after initializaion of state
-    from api.graphql.bindings import generate_schema
-    schema = generate_schema()
-
-    in_spec = app.state.vyos_introspection
-
-    if app.state.vyos_origins:
-        origins = app.state.vyos_origins
-        app.add_route('/graphql', CORSMiddleware(GraphQL(schema,
-                                                         context_value=get_user_context,
-                                                         debug=True,
-                                                         introspection=in_spec),
-                                                 allow_origins=origins,
-                                                 allow_methods=("GET", "POST", "OPTIONS"),
-                                                 allow_headers=("Authorization",)))
-    else:
-        app.add_route('/graphql', GraphQL(schema,
-                                          context_value=get_user_context,
-                                          debug=True,
-                                          introspection=in_spec))
 ###
 # Modify uvicorn to allow reloading server within the configsession
 ###
 
 server = None
 shutdown = False
 
+
 class ApiServerConfig(UvicornConfig):
     pass
 
+
 class ApiServer(UvicornServer):
     def install_signal_handlers(self):
         pass
 
+
 def reload_handler(signum, frame):
+    # pylint: disable=global-statement
+
     global server
-    logger.debug('Reload signal received...')
+    LOG.debug('Reload signal received...')
     if server is not None:
         server.handle_exit(signum, frame)
         server = None
-        logger.info('Server stopping for reload...')
+        LOG.info('Server stopping for reload...')
     else:
-        logger.warning('Reload called for non-running server...')
+        LOG.warning('Reload called for non-running server...')
+
 
 def shutdown_handler(signum, frame):
+    # pylint: disable=global-statement
+
     global shutdown
-    logger.debug('Shutdown signal received...')
+    LOG.debug('Shutdown signal received...')
     server.handle_exit(signum, frame)
-    logger.info('Server shutdown...')
+    LOG.info('Server shutdown...')
     shutdown = True
 
+# end modify uvicorn
+
+
 def flatten_keys(d: dict) -> list[dict]:
     keys_list = []
     for el in list(d['keys'].get('id', {})):
         key = d['keys']['id'][el].get('key', '')
         if key:
             keys_list.append({'id': el, 'key': key})
     return keys_list
 
-def initialization(session: ConfigSession, app: FastAPI = app):
+
+def regenerate_docs(app: FastAPI) -> None:
+    docs = ('/openapi.json', '/docs', '/docs/oauth2-redirect', '/redoc')
+    remove = []
+    for r in app.routes:
+        if r.path in docs:
+            remove.append(r)
+    for r in remove:
+        app.routes.remove(r)
+
+    app.openapi_schema = None
+    app.setup()
+
+
+def initialization(session: SessionState, app: FastAPI = app):
+    # pylint: disable=global-statement,broad-exception-caught,import-outside-toplevel
+
     global server
     try:
         server_config = load_server_config()
     except Exception as e:
-        logger.critical(f'Failed to load the HTTP API server config: {e}')
+        LOG.critical(f'Failed to load the HTTP API server config: {e}')
         sys.exit(1)
 
-    app.state.vyos_session = session
-    app.state.vyos_keys = []
-
     if 'keys' in server_config:
-        app.state.vyos_keys = flatten_keys(server_config)
+        session.keys = flatten_keys(server_config)
+
+    rest_config = server_config.get('rest', {})
+    session.debug = bool('debug' in rest_config)
+    session.strict = bool('strict' in rest_config)
+
+    graphql_config = server_config.get('graphql', {})
+    session.origins = graphql_config.get('cors', {}).get('allow_origin', [])
+
+    if 'rest' in server_config:
+        session.rest = True
+    else:
+        session.rest = False
 
-    app.state.vyos_debug = bool('debug' in server_config)
-    app.state.vyos_strict = bool('strict' in server_config)
-    app.state.vyos_origins = server_config.get('cors', {}).get('allow_origin', [])
     if 'graphql' in server_config:
-        app.state.vyos_graphql = True
+        session.graphql = True
         if isinstance(server_config['graphql'], dict):
             if 'introspection' in server_config['graphql']:
-                app.state.vyos_introspection = True
+                session.introspection = True
             else:
-                app.state.vyos_introspection = False
+                session.introspection = False
             # default values if not set explicitly
-            app.state.vyos_auth_type = server_config['graphql']['authentication']['type']
-            app.state.vyos_token_exp = server_config['graphql']['authentication']['expiration']
-            app.state.vyos_secret_len = server_config['graphql']['authentication']['secret_length']
+            session.auth_type = server_config['graphql']['authentication']['type']
+            session.token_exp = server_config['graphql']['authentication']['expiration']
+            session.secret_len = server_config['graphql']['authentication']['secret_length']
+    else:
+        session.graphql = False
+
+    # pass session state
+    app.state = session
+
+    # add REST routes
+    if session.rest:
+        from api.rest.routers import rest_init
+        rest_init(app)
     else:
-        app.state.vyos_graphql = False
+        from api.rest.routers import rest_clear
+        rest_clear(app)
 
-    if app.state.vyos_graphql:
+    # add GraphQL route
+    if session.graphql:
+        from api.graphql.routers import graphql_init
         graphql_init(app)
+    else:
+        from api.graphql.routers import graphql_clear
+        graphql_clear(app)
+
+    regenerate_docs(app)
+
+    LOG.debug('Active routes are:')
+    for r in app.routes:
+        LOG.debug(f'{r.path}')
 
     config = ApiServerConfig(app, uds="/run/api.sock", proxy_headers=True)
     server = ApiServer(config)
 
-def run_server():
-    try:
-        server.run()
-    except OSError as e:
-        logger.critical(e)
-        sys.exit(1)
 
 if __name__ == '__main__':
     # systemd's user and group options don't work, do it by hand here,
     # else no one else will be able to commit
     cfg_group = grp.getgrnam(CFG_GROUP)
     os.setgid(cfg_group.gr_gid)
 
     # Need to set file permissions to 775 too so that every vyattacfg group member
     # has write access to the running config
     os.umask(0o002)
 
     signal.signal(signal.SIGHUP, reload_handler)
     signal.signal(signal.SIGTERM, shutdown_handler)
 
-    config_session = ConfigSession(os.getpid())
+    session_state = SessionState()
+    session_state.session = ConfigSession(os.getpid())
 
     while True:
-        logger.debug('Enter main loop...')
+        LOG.debug('Enter main loop...')
         if shutdown:
             break
         if server is None:
-            initialization(config_session)
+            initialization(session_state)
             server.run()
         sleep(1)