diff --git a/data/templates/https/nginx.default.tmpl b/data/templates/https/nginx.default.tmpl
index 5459fe98d..b40ddcc74 100644
--- a/data/templates/https/nginx.default.tmpl
+++ b/data/templates/https/nginx.default.tmpl
@@ -1,73 +1,73 @@
 ### Autogenerated by https.py ###
 # Default server configuration
 #
 server {
         listen 80 default_server;
         listen [::]:80 default_server;
         server_name _;
         return 301 https://$host$request_uri;
 }
 
 {% for server in server_block_list %}
 server {
 
         # SSL configuration
         #
 {% if server.address == '*' %}
         listen {{ server.port }} ssl;
         listen [::]:{{ server.port }} ssl;
 {% else %}
         listen {{ server.address }}:{{ server.port }} ssl;
 {% endif %}
 
 {% for name in server.name %}
         server_name {{ name }};
 {% endfor %}
 
 {% if server.certbot %}
         ssl_certificate {{ server.certbot_dir }}/live/{{ server.certbot_domain_dir }}/fullchain.pem;
         ssl_certificate_key {{ server.certbot_dir }}/live/{{ server.certbot_domain_dir }}/privkey.pem;
         include {{ server.certbot_dir }}/options-ssl-nginx.conf;
         ssl_dhparam {{ server.certbot_dir }}/ssl-dhparams.pem;
 {% elif server.vyos_cert %}
         ssl_certificate {{ server.vyos_cert.crt }};
         ssl_certificate_key {{ server.vyos_cert.key }};
 {% else %}
         #
         # Self signed certs generated by the ssl-cert package
         # Don't use them in a production server!
         #
         include snippets/snakeoil.conf;
 {% endif %}
 
         # proxy settings for HTTP API, if enabled; 503, if not
-        location ~ /(retrieve|configure|config-file|image|generate|show|docs|openapi.json|redoc) {
+        location ~ /(retrieve|configure|config-file|image|generate|show|docs|openapi.json|redoc|graphql) {
 {% if server.api %}
                 proxy_pass http://localhost:{{ server.api.port }};
                 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                 proxy_set_header X-Forwarded-Proto $scheme;
                 proxy_read_timeout 600;
                 proxy_buffering off;
 {% else %}
                 return 503;
 {% endif %}
         }
 
         error_page 497 =301 https://$host:{{ server.port }}$request_uri;
         error_page 501 502 503 =200 @50*_json;
 
 {% if api_set %}
         location @50*_json {
                 default_type application/json;
                 return 200 '{"error": "service https api unavailable at this proxy address: set service https api-restrict virtual-host"}';
         }
 {% else %}
         location @50*_json {
                 default_type application/json;
                 return 200 '{"error": "Start service in configuration mode: set service https api"}';
         }
 {% endif %}
 
 }
 
 {% endfor %}
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 9921e3b5f..03006c383 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -1,53 +1,56 @@
 # Copyright 2018 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/>.
 
 
 directories = {
   "data": "/usr/share/vyos/",
   "conf_mode": "/usr/libexec/vyos/conf_mode",
   "config": "/opt/vyatta/etc/config",
   "current": "/opt/vyatta/etc/config-migrate/current",
   "migrate": "/opt/vyatta/etc/config-migrate/migrate",
   "log": "/var/log/vyatta",
   "templates": "/usr/share/vyos/templates/",
-  "certbot": "/config/auth/letsencrypt"
+  "certbot": "/config/auth/letsencrypt",
+  "api_schema": "/usr/libexec/vyos/services/api/graphql/graphql/schema/",
+  "api_templates": "/usr/libexec/vyos/services/api/graphql/recipes/templates/"
+
 }
 
 cfg_group = 'vyattacfg'
 
 cfg_vintage = 'vyos'
 
 commit_lock = '/opt/vyatta/config/.lock'
 
 version_file = '/usr/share/vyos/component-versions.json'
 
 https_data = {
     'listen_addresses' : { '*': ['_'] }
 }
 
 api_data = {
     'listen_address' : '127.0.0.1',
     'port' : '8080',
     'strict' : 'false',
     'debug' : 'false',
     'api_keys' : [ {"id": "testapp", "key": "qwerty"} ]
 }
 
 vyos_cert_data = {
     "conf": "/etc/nginx/snippets/vyos-cert.conf",
     "crt": "/etc/ssl/certs/vyos-selfsigned.crt",
     "key": "/etc/ssl/private/vyos-selfsign",
     "lifetime": "365",
 }
diff --git a/src/services/api/graphql/graphql/__init__.py b/src/services/api/graphql/graphql/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py
new file mode 100644
index 000000000..651421c35
--- /dev/null
+++ b/src/services/api/graphql/graphql/directives.py
@@ -0,0 +1,17 @@
+from ariadne import SchemaDirectiveVisitor, ObjectType
+from . mutations import make_resolver
+
+class DataDirective(SchemaDirectiveVisitor):
+    """
+    Class providing implementation of 'generate' directive in schema.
+
+    """
+    def visit_field_definition(self, field, object_type):
+        name = f'{field.type}'
+        # field.type contains the return value of the mutation; trim value
+        # to produce canonical name
+        name = name.replace('Result', '', 1)
+
+        func = make_resolver(name)
+        field.resolve = func
+        return field
diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py
new file mode 100644
index 000000000..af779d06f
--- /dev/null
+++ b/src/services/api/graphql/graphql/mutations.py
@@ -0,0 +1,60 @@
+
+from importlib import import_module
+from typing import Any, Dict
+from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake
+from graphql import GraphQLResolveInfo
+from makefun import with_signature
+
+from .. import state
+
+mutation = ObjectType("Mutation")
+
+def make_resolver(mutation_name):
+    """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:
+        encapsulating ConfigErrors, or internal errors
+    """
+    class_name = mutation_name.replace('create', '', 1).replace('delete', '', 1)
+    func_base_name = convert_camel_case_to_snake(class_name)
+    resolver_name = f'resolve_create_{func_base_name}'
+    func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)'
+
+    @mutation.field(mutation_name)
+    @convert_kwargs_to_snake_case
+    @with_signature(func_sig, func_name=resolver_name)
+    async def func_impl(*args, **kwargs):
+        try:
+            if 'data' not in kwargs:
+                return {
+                    "success": False,
+                    "errors": ['missing data']
+                }
+
+            data = kwargs['data']
+            session = state.settings['app'].state.vyos_session
+
+            mod = import_module(f'api.recipes.{func_base_name}')
+            klass = getattr(mod, class_name)
+            k = klass(session, data)
+            k.configure()
+
+            return {
+                "success": True,
+                "data": data
+            }
+        except Exception as error:
+            return {
+                "success": False,
+                "errors": [str(error)]
+            }
+
+    return func_impl
+
+
diff --git a/src/services/api/graphql/graphql/schema/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql
new file mode 100644
index 000000000..a7ee75d40
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/dhcp_server.graphql
@@ -0,0 +1,35 @@
+input dhcpServerConfigInput {
+    sharedNetworkName: String
+    subnet: String
+    defaultRouter: String
+    dnsServer: String
+    domainName: String
+    lease: Int
+    range: Int
+    start: String
+    stop: String
+    dnsForwardingAllowFrom: String
+    dnsForwardingCacheSize: Int
+    dnsForwardingListenAddress: String
+}
+
+type dhcpServerConfig {
+    sharedNetworkName: String
+    subnet: String
+    defaultRouter: String
+    dnsServer: String
+    domainName: String
+    lease: Int
+    range: Int
+    start: String
+    stop: String
+    dnsForwardingAllowFrom: String
+    dnsForwardingCacheSize: Int
+    dnsForwardingListenAddress: String
+}
+
+type createDhcpServerResult {
+    data: dhcpServerConfig
+    success: Boolean!
+    errors: [String]
+}
diff --git a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
new file mode 100644
index 000000000..fdcf97bad
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
@@ -0,0 +1,18 @@
+input interfaceEthernetConfigInput {
+    interface: String
+    address: String
+    replace: Boolean = true
+    description: String
+}
+
+type interfaceEthernetConfig {
+    interface: String
+    address: String
+    description: String
+}
+
+type createInterfaceEthernetResult {
+    data: interfaceEthernetConfig
+    success: Boolean!
+    errors: [String]
+}
diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql
new file mode 100644
index 000000000..8a5e17962
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/schema.graphql
@@ -0,0 +1,15 @@
+schema {
+    query: Query
+    mutation: Mutation
+}
+
+type Query {
+    _dummy: String
+}
+
+directive @generate on FIELD_DEFINITION
+
+type Mutation {
+    createDhcpServer(data: dhcpServerConfigInput) : createDhcpServerResult @generate
+    createInterfaceEthernet(data: interfaceEthernetConfigInput) : createInterfaceEthernetResult @generate
+}
diff --git a/src/services/api/graphql/recipes/__init__.py b/src/services/api/graphql/recipes/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/services/api/graphql/recipes/dhcp_server.py b/src/services/api/graphql/recipes/dhcp_server.py
new file mode 100644
index 000000000..3edb3028e
--- /dev/null
+++ b/src/services/api/graphql/recipes/dhcp_server.py
@@ -0,0 +1,13 @@
+
+from . recipe import Recipe
+
+class DhcpServer(Recipe):
+    def __init__(self, session, command_file):
+        super().__init__(session, command_file)
+
+    # Define any custom processing of parameters here by overriding
+    # configure:
+    #
+    # def configure(self):
+    #     self.data = transform_data(self.data)
+    #     super().configure()
diff --git a/src/services/api/graphql/recipes/interface_ethernet.py b/src/services/api/graphql/recipes/interface_ethernet.py
new file mode 100644
index 000000000..f88f5924f
--- /dev/null
+++ b/src/services/api/graphql/recipes/interface_ethernet.py
@@ -0,0 +1,13 @@
+
+from . recipe import Recipe
+
+class InterfaceEthernet(Recipe):
+    def __init__(self, session, command_file):
+        super().__init__(session, command_file)
+
+    # Define any custom processing of parameters here by overriding
+    # configure:
+    #
+    # def configure(self):
+    #     self.data = transform_data(self.data)
+    #     super().configure()
diff --git a/src/services/api/graphql/recipes/recipe.py b/src/services/api/graphql/recipes/recipe.py
new file mode 100644
index 000000000..8fbb9e0bf
--- /dev/null
+++ b/src/services/api/graphql/recipes/recipe.py
@@ -0,0 +1,49 @@
+from ariadne import convert_camel_case_to_snake
+import vyos.defaults
+from vyos.template import render
+
+class Recipe(object):
+    def __init__(self, session, data):
+        self._session = session
+        self.data = data
+        self._name = convert_camel_case_to_snake(type(self).__name__)
+
+    @property
+    def data(self):
+        return self.__data
+
+    @data.setter
+    def data(self, data):
+        if isinstance(data, dict):
+            self.__data = data
+        else:
+            raise ValueError("data must be of type dict")
+
+    def configure(self):
+        session = self._session
+        data = self.data
+        func_base_name = self._name
+
+        tmpl_file = f'{func_base_name}.tmpl'
+        cmd_file = f'/tmp/{func_base_name}.cmds'
+        tmpl_dir = vyos.defaults.directories['api_templates']
+
+        try:
+            render(cmd_file, tmpl_file, data, location=tmpl_dir)
+            commands = []
+            with open(cmd_file) as f:
+                lines = f.readlines()
+            for line in lines:
+                commands.append(line.split())
+            for cmd in commands:
+                if cmd[0] == 'set':
+                    session.set(cmd[1:])
+                elif cmd[0] == 'delete':
+                    session.delete(cmd[1:])
+                else:
+                    raise ValueError('Operation must be "set" or "delete"')
+            session.commit()
+        except Exception as error:
+            raise error
+
+
diff --git a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl b/src/services/api/graphql/recipes/templates/dhcp_server.tmpl
new file mode 100644
index 000000000..629ce83c1
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/dhcp_server.tmpl
@@ -0,0 +1,9 @@
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} default-router {{ default_router }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} dns-server {{ dns_server }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} domain-name {{ domain_name }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} lease {{ lease }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} start {{ start }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} stop {{ stop }}
+set service dns forwarding allow-from {{ dns_forwarding_allow_from }}
+set service dns forwarding cache-size {{ dns_forwarding_cache_size }}
+set service dns forwarding listen-address {{ dns_forwarding_listen_address }}
diff --git a/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl b/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl
new file mode 100644
index 000000000..d9d7ed691
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl
@@ -0,0 +1,5 @@
+{% if replace %}
+delete interfaces ethernet {{ interface }} address
+{% endif %}
+set interfaces ethernet {{ interface }} address {{ address }}
+set interfaces ethernet {{ interface }} description {{ description }}
diff --git a/src/services/api/graphql/state.py b/src/services/api/graphql/state.py
new file mode 100644
index 000000000..63db9f4ef
--- /dev/null
+++ b/src/services/api/graphql/state.py
@@ -0,0 +1,4 @@
+
+def init():
+    global settings
+    settings = {}
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index cbf321dc8..cd7c92270 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -1,635 +1,662 @@
 #!/usr/share/vyos-http-api-tools/bin/python3
 #
 # Copyright (C) 2019-2021 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 traceback
 import threading
 from typing import List, Union, Callable, Dict
 
 import uvicorn
 from fastapi import FastAPI, Depends, Request, Response, HTTPException
 from fastapi.responses import HTMLResponse
 from fastapi.exceptions import RequestValidationError
 from fastapi.routing import APIRoute
 from pydantic import BaseModel, StrictStr, validator
 from starlette.datastructures import FormData, MutableHeaders
 from starlette.formparsers import FormParser, MultiPartParser
 from multipart.multipart import parse_options_header
 
+from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers
+from ariadne.asgi import GraphQL
+
 import vyos.config
+import vyos.defaults
 
 from vyos.configsession import ConfigSession, ConfigSessionError
 
+import api.state
+
 DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf'
 CFG_GROUP = 'vyattacfg'
 
 debug = True
 
 logger = logging.getLogger(__name__)
 logs_handler = logging.StreamHandler()
 logger.addHandler(logs_handler)
 
 if debug:
     logger.setLevel(logging.DEBUG)
 else:
     logger.setLevel(logging.INFO)
 
 # Giant lock!
 lock = threading.Lock()
 
 def load_server_config():
     with open(DEFAULT_CONFIG_FILE) as f:
         config = json.load(f)
     return config
 
 def check_auth(key_list, key):
     id = None
     for k in key_list:
         if k['key'] == key:
             id = k['id']
     return 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 BaseConfigureModel(BaseModel):
     op: StrictStr
     path: List[StrictStr]
     value: StrictStr = None
 
     @validator("path", pre=True, always=True)
     def check_non_empty(cls, path):
         assert len(path) > 0
         return path
 
 class ConfigureModel(ApiModel):
     op: StrictStr
     path: List[StrictStr]
     value: StrictStr = None
 
     @validator("path", pre=True, always=True)
     def check_non_empty(cls, path):
         assert len(path) > 0
         return path
 
     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 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 ImageModel(ApiModel):
     op: StrictStr
     url: StrictStr = None
     name: StrictStr = None
 
     class Config:
         schema_extra = {
             "example": {
                 "key": "id_key",
                 "op": "add | delete",
                 "url": "imagelocation",
                 "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 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
     id = check_auth(api_keys, key)
     if not id:
         raise HTTPException(status_code=401, detail="Valid API key is required")
     app.state.vyos_id = 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):
     ERR_MISSING_KEY = False
     ERR_MISSING_DATA = False
     ERR_NOT_JSON = False
     ERR_NOT_DICT = False
     ERR_NO_OP = False
     ERR_NO_PATH = False
     ERR_EMPTY_PATH = False
     ERR_PATH_NOT_LIST = False
     ERR_VALUE_NOT_STRING = False
     ERR_PATH_NOT_LIST_OF_STR = False
     offending_command = {}
     exception = None
 
     @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 not hasattr(self, "_form"):
             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:
                 logger.debug("processing form data")
                 for k, v in form_data.multi_items():
                     forms[k] = v
 
                 if 'data' not in forms:
                     self.ERR_MISSING_DATA = True
                 else:
                     try:
                         tmp = json.loads(forms['data'])
                     except json.JSONDecodeError as e:
                         self.ERR_NOT_JSON = True
                         self.exception = e
                         tmp = {}
                     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.ERR_NOT_DICT = True
                         self.offending_command = c
                     elif 'op' not in c:
                         self.ERR_NO_OP = True
                         self.offending_command = c
                     elif 'path' not in c:
                         self.ERR_NO_PATH = True
                         self.offending_command = c
                     elif not c['path']:
                         self.ERR_EMPTY_PATH = True
                         self.offending_command = c
                     elif not isinstance(c['path'], list):
                         self.ERR_PATH_NOT_LIST = True
                         self.offending_command = c
                     elif not all(isinstance(el, str) for el in c['path']):
                         self.ERR_PATH_NOT_LIST_OF_STR = True
                         self.offending_command = c
                     elif 'value' in c and not isinstance(c['value'], str):
                         self.ERR_VALUE_NOT_STRING = True
                         self.offending_command = c
 
                 if 'key' not in forms and 'key' not in merge:
                     self.ERR_MISSING_KEY = True
                 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)
             endpoint = request.url.path
             try:
                 response: Response = await original_route_handler(request)
             except HTTPException as e:
                 return error(e.status_code, e.detail)
             except Exception as e:
                 if request.ERR_MISSING_KEY:
                     return error(422, "Valid API key is required")
                 if request.ERR_MISSING_DATA:
                     return error(422, "Non-empty data field is required")
                 if request.ERR_NOT_JSON:
                     return error(400, "Failed to parse JSON: {0}".format(request.exception))
                 if endpoint == '/configure':
                     if request.ERR_NOT_DICT:
                         return error(400, "Malformed command \"{0}\": any command must be a dict".format(json.dumps(request.offending_command)))
                     if request.ERR_NO_OP:
                         return error(400, "Malformed command \"{0}\": missing \"op\" field".format(json.dumps(request.offending_command)))
                     if request.ERR_NO_PATH:
                         return error(400, "Malformed command \"{0}\": missing \"path\" field".format(json.dumps(request.offending_command)))
                     if request.ERR_EMPTY_PATH:
                         return error(400, "Malformed command \"{0}\": empty path".format(json.dumps(request.offending_command)))
                     if request.ERR_PATH_NOT_LIST:
                         return error(400, "Malformed command \"{0}\": \"path\" field must be a list".format(json.dumps(request.offending_command)))
                     if request.ERR_VALUE_NOT_STRING:
                         return error(400, "Malformed command \"{0}\": \"value\" field must be a string".format(json.dumps(request.offending_command)))
                     if request.ERR_PATH_NOT_LIST_OF_STR:
                         return error(400, "Malformed command \"{0}\": \"path\" field must be a list of strings".format(json.dumps(request.offending_command)))
                 if endpoint in ('/retrieve','/generate','/show'):
                     if request.ERR_NO_OP or request.ERR_NO_PATH:
                         return error(400, "Missing required field. \"op\" and \"path\" fields are required")
                 if endpoint in ('/config-file', '/image'):
                     if request.ERR_NO_OP:
                         return error(400, "Missing required field \"op\"")
 
                 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
 
 @app.exception_handler(RequestValidationError)
 async def validation_exception_handler(request, exc):
     return error(400, str(exc.errors()[0]))
 
 @app.post('/configure')
 def configure_op(data: Union[ConfigureModel, ConfigureListModel]):
     session = app.state.vyos_session
     env = session.get_session_env()
     config = vyos.config.Config(session_env=env)
 
     # Allow users to pass just one command
     if not isinstance(data, ConfigureListModel):
         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()
 
     status = 200
     error_msg = None
     try:
         for c in data:
             op = c.op
             path = c.path
 
             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()
 
             if op == 'set':
                 # XXX: it would be nice to do a strict check for "path already exists",
                 # but there's probably no way to do that
                 session.set(path, value=value)
             elif op == 'delete':
                 if app.state.vyos_strict and not config.exists(cfg_path):
                     raise ConfigSessionError("Cannot delete [{0}]: path/value does not exist".format(cfg_path))
                 session.delete(path, value=value)
             elif op == 'comment':
                 session.comment(path, value=value)
             else:
                 raise ConfigSessionError("\"{0}\" is not a valid operation".format(op))
         # end for
         session.commit()
         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(None)
 
 @app.post("/retrieve")
 def retrieve_op(data: RetrieveModel):
     session = app.state.vyos_session
     env = session.get_session_env()
     config = vyos.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 = vyos.configtree.ConfigTree(res)
                 res = json.loads(config_tree.to_json())
             elif config_format == 'json_ast':
                 config_tree = vyos.configtree.ConfigTree(res)
                 res = json.loads(config_tree.to_json_ast())
             elif config_format == 'raw':
                 pass
             else:
                 return error(400, "\"{0}\" is not a valid config format".format(config_format))
         else:
             return error(400, "\"{0}\" is not a valid operation".format(op))
     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):
     session = app.state.vyos_session
 
     op = data.op
 
     try:
         if op == 'save':
             if data.file:
                 path = data.file
             else:
                 path = '/config/config.boot'
             res = session.save_config(path)
         elif op == 'load':
             if data.file:
                 path = data.file
             else:
                 return error(400, "Missing required field \"file\"")
             res = session.migrate_and_load_config(path)
             res = session.commit()
         else:
             return error(400, "\"{0}\" is not a valid operation".format(op))
     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('/image')
 def image_op(data: ImageModel):
     session = app.state.vyos_session
 
     op = data.op
 
     try:
         if op == 'add':
             if data.url:
                 url = data.url
             else:
                 return error(400, "Missing required field \"url\"")
             res = session.install_image(url)
         elif op == 'delete':
             if data.name:
                 name = data.name
             else:
                 return error(400, "Missing required field \"name\"")
             res = session.remove_image(name)
         else:
             return error(400, "\"{0}\" is not a valid operation".format(op))
     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, "\"{0}\" is not a valid operation".format(op))
     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, "\"{0}\" is not a valid operation".format(op))
     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
+###
+
+api.state.init()
+
+from api.graphql.mutations import mutation
+from api.graphql.directives import DataDirective
+
+api_schema_dir = vyos.defaults.directories['api_schema']
+
+type_defs = load_schema_from_path(api_schema_dir)
+
+schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective})
+
+app.add_route('/graphql', GraphQL(schema, debug=True))
+
+###
+
 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)
 
     try:
         server_config = load_server_config()
     except Exception as e:
         logger.critical("Failed to load the HTTP API server config: {0}".format(e))
 
     session = ConfigSession(os.getpid())
 
     app.state.vyos_session = session
     app.state.vyos_keys = server_config['api_keys']
 
     app.state.vyos_debug = True if server_config['debug'] == 'true' else False
     app.state.vyos_strict = True if server_config['strict'] == 'true' else False
 
+    api.state.settings['app'] = app
+
     try:
         uvicorn.run(app, host=server_config["listen_address"],
                          port=int(server_config["port"]),
                          proxy_headers=True)
     except OSError as e:
         logger.critical(f"OSError {e}")
         sys.exit(1)