diff --git a/bin/yunohost b/bin/yunohost
index d4483df5..fb03b34d 100755
--- a/bin/yunohost
+++ b/bin/yunohost
@@ -5,25 +5,29 @@ import sys
 import os.path
 import gettext
 
-# Debug option
-if '--debug' in sys.argv:
-    sys.path.append(os.path.abspath(os.path.dirname(__file__) +'/../src'))
-from moulinette import cli
-from moulinette.core.helpers import YunoHostError, colorize
+# Run from source
+basedir = os.path.abspath(os.path.dirname(__file__) +'/../')
+if os.path.isdir(basedir +'/src'):
+    sys.path.append(basedir +'/src')
 
-gettext.install('YunoHost')
+from moulinette import init, cli, MoulinetteError
+from moulinette.helpers import YunoHostError, colorize
+
+gettext.install('yunohost')
 
 
 ## Main action
 
 if __name__ == '__main__':
+    # Run from source (prefix and libdir set to None)
+    init('yunohost', prefix=None, libdir=None,
+         cachedir=os.path.join(basedir, 'cache'))
+
     # Additional arguments
     use_cache = True
     if '--no-cache' in sys.argv:
         use_cache = False
         sys.argv.remove('--no-cache')
-    if '--debug' in sys.argv:
-        sys.argv.remove('--debug')
 
     try:
         args = list(sys.argv)
@@ -36,6 +40,9 @@ if __name__ == '__main__':
 
         # Execute the action
         cli(args, use_cache)
+    except MoulinetteError as e:
+        print(e.colorize())
+        sys.exit(e.code)
     except YunoHostError as e:
         print(colorize(_("Error: "), 'red') + e.message)
         sys.exit(e.code)
diff --git a/bin/yunohost-api b/bin/yunohost-api
index 625d2b7e..18d10d09 100755
--- a/bin/yunohost-api
+++ b/bin/yunohost-api
@@ -5,12 +5,14 @@ import sys
 import os.path
 import gettext
 
-# Debug option
-if '--debug' in sys.argv:
-    sys.path.append(os.path.abspath(os.path.dirname(__file__) +'/../src'))
-from moulinette import api
+# Run from source
+basedir = os.path.abspath(os.path.dirname(__file__) +'/../')
+if os.path.isdir(basedir +'/src'):
+    sys.path.append(basedir +'/src')
 
-gettext.install('YunoHost')
+from moulinette import init, api
+
+gettext.install('yunohost')
 
 
 ## Callbacks for additional routes
@@ -29,6 +31,10 @@ def is_installed():
 ## Main action
 
 if __name__ == '__main__':
+    # Run from source (prefix and libdir set to None)
+    init('yunohost', prefix=None, libdir=None,
+         cachedir=os.path.join(basedir, 'cache'))
+
     # Additional arguments
     use_cache = True
     if '--no-cache' in sys.argv:
diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml
index fe5ab237..52a62654 100644
--- a/data/actionsmap/yunohost.yml
+++ b/data/actionsmap/yunohost.yml
@@ -76,7 +76,9 @@ user:
                     help: Must be unique
                     extra:
                         ask: "Username"
-                        pattern: '^[a-z0-9_]+$'
+                        pattern:
+                            - '^[a-z0-9_]+$'
+                            - "Must be alphanumeric and underscore characters only"
                 -f:
                     full: --firstname
                     extra:
@@ -109,7 +111,9 @@ user:
                     nargs: "*"
                     extra:
                         ask: "Users to delete"
-                        pattern: '^[a-z0-9_]+$'
+                        pattern:
+                            - '^[a-z0-9_]+$'
+                            - "Must be alphanumeric and underscore characters only"
                 --purge:
                     action: store_true
 
@@ -187,7 +191,9 @@ domain:
                     help: Domain name to add
                     nargs: '+'
                     extra:
-                        pattern: '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
+                        pattern:
+                            - '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
+                            - "Must be a valid domain name (e.g. my-domain.org)"
                 -m:
                     full: --main
                     help: Is the main domain
@@ -206,7 +212,9 @@ domain:
                     help: Domain(s) to delete
                     nargs: "+"
                     extra:
-                        pattern: '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
+                        pattern:
+                            - '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
+                            - "Must be a valid domain name (e.g. my-domain.org)"
 
         ### domain_info()
         info:
@@ -216,7 +224,9 @@ domain:
                 domain:
                     help: ""
                     extra:
-                        pattern: '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
+                        pattern:
+                            - '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
+                            - "Must be a valid domain name (e.g. my-domain.org)"
 
 
 #############################
@@ -253,7 +263,9 @@ app:
                     help: Name of the list to remove
                     extra:
                         ask: "List to remove"
-                        pattern: '^[a-z0-9_]+$'
+                        pattern:
+                            - '^[a-z0-9_]+$'
+                            - "Must be alphanumeric and underscore characters only"
 
         ### app_list()
         list:
@@ -302,7 +314,9 @@ app:
                     full: --user
                     help: Allowed app map for a user
                     extra:
-                        pattern: '^[a-z0-9_]+$'
+                        pattern:
+                            - '^[a-z0-9_]+$'
+                            - "Must be alphanumeric and underscore characters only"
 
 
         ### app_install() TODO: Write help
@@ -388,7 +402,9 @@ app:
                 port:
                     help: Port to check
                     extra:
-                        pattern: '^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$'
+                        pattern:
+                            - '^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$'
+                            - "Must be a valid port number (i.e. 0-65535)"
 
         ### app_checkurl()
         checkurl:
@@ -659,7 +675,9 @@ service:
                     help: Number of lines to display
                     default: "50"
                     extra:
-                        pattern: '^[0-9]+$'
+                        pattern:
+                            - '^[0-9]+$'
+                            - "Must be a valid number"
 
 
 #############################
@@ -691,7 +709,9 @@ firewall:
                 port:
                     help: Port to open
                     extra:
-                        pattern: '^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$'
+                        pattern:
+                            - '^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$'
+                            - "Must be a valid port number (i.e. 0-65535)"
                 protocol:
                     help: Protocol associated with port
                     choices:
@@ -840,12 +860,16 @@ tools:
                 -o:
                     full: --old-domain
                     extra:
-                        pattern: '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
+                        pattern:
+                            - '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
+                            - "Must be a valid domain name (e.g. my-domain.org)"
                 -n:
                     full: --new-domain
                     extra:
                         ask: "New main domain"
-                        pattern: '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
+                        pattern:
+                            - '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
+                            - "Must be a valid domain name (e.g. my-domain.org)"
 
         ### tools_postinstall()
         postinstall:
@@ -857,7 +881,9 @@ tools:
                     help: YunoHost main domain
                     extra:
                         ask: "Main domain"
-                        pattern: '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
+                        pattern:
+                            - '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
+                            - "Must be a valid domain name (e.g. my-domain.org)"
                 -p:
                     full: --password
                     help: YunoHost admin password
diff --git a/src/moulinette/__init__.py b/src/moulinette/__init__.py
index f96eae48..aa8c6753 100755
--- a/src/moulinette/__init__.py
+++ b/src/moulinette/__init__.py
@@ -24,68 +24,98 @@ __credits__ = """
     You should have received a copy of the GNU Affero General Public License
     along with this program; if not, see http://www.gnu.org/licenses
     """
+__all__ = [
+    'init', 'api', 'cli',
+    'MoulinetteError',
+]
+
+from .core import MoulinetteError
+
+curr_namespace = None
 
 
-## Fast access functions
+## Package functions
+
+def init(namespace=None, **kwargs):
+    """Package initialization
+
+    Initialize directories and global variables. It must be called
+    before any of package method is used - even the easy access
+    functions.
+
+    Keyword arguments:
+        - namespace -- The namespace to initialize and use
+        - **kwargs -- See helpers.Package
+
+    At the end, the global variable 'pkg' will contain a Package
+    instance. See helpers.Package for available methods and variables.
+
+    """
+    import __builtin__
+    from .core import Package
+
+    global curr_namespace
+    curr_namespace = namespace
+
+    __builtin__.__dict__['pkg'] = Package(**kwargs)
+
+
+## Easy access to interfaces
 
 def api(port, routes={}, use_cache=True):
-    """
+    """Web server (API) interface
+
     Run a HTTP server with the moulinette for an API usage.
 
     Keyword arguments:
-
         - port -- Port to run on
-
         - routes -- A dict of additional routes to add in the form of
             {(method, uri): callback}
-
         - use_cache -- False if it should parse the actions map file
             instead of using the cached one
 
     """
     from bottle import run
-    from core.actionsmap import ActionsMap
-    from core.api import MoulinetteAPI
-    from core.helpers import Interface
+    from .actionsmap import ActionsMap
+    from .interface.api import MoulinetteAPI
 
-    amap = ActionsMap(Interface.api, use_cache=use_cache)
+    amap = ActionsMap('api', use_cache=use_cache)
     moulinette = MoulinetteAPI(amap, routes)
 
     run(moulinette.app, port=port)
 
 def cli(args, use_cache=True):
-    """
+    """Command line interface
+
     Execute an action with the moulinette from the CLI and print its
     result in a readable format.
 
     Keyword arguments:
-
         - args -- A list of argument strings
-
         - use_cache -- False if it should parse the actions map file
             instead of using the cached one
 
     """
     import os
-    from core.actionsmap import ActionsMap
-    from core.helpers import Interface, YunoHostError, pretty_print_dict
+    from .actionsmap import ActionsMap
+    from .helpers import YunoHostError, pretty_print_dict
 
     lock_file = '/var/run/moulinette.lock'
 
     # TODO: Move the lock checking into the ActionsMap class
     # Check the lock
     if os.path.isfile(lock_file):
-        raise YunoHostError(1, _("The moulinette is already running"))
+        raise MoulinetteError(1, _("The moulinette is already running"))
 
     # Create a lock
     with open(lock_file, 'w') as f: pass
     os.system('chmod 400 '+ lock_file)
 
     try:
-        amap = ActionsMap(Interface.cli, use_cache=use_cache)
+        amap = ActionsMap('cli', use_cache=use_cache)
         pretty_print_dict(amap.process(args))
     except KeyboardInterrupt, EOFError:
-        raise YunoHostError(125, _("Interrupted"))
+        raise MoulinetteError(125, _("Interrupted"))
     finally:
         # Remove the lock
         os.remove(lock_file)
diff --git a/src/moulinette/actionsmap.py b/src/moulinette/actionsmap.py
new file mode 100644
index 00000000..f2eb5464
--- /dev/null
+++ b/src/moulinette/actionsmap.py
@@ -0,0 +1,338 @@
+# -*- coding: utf-8 -*-
+
+import pickle
+import yaml
+import re
+import os
+from collections import OrderedDict
+
+import logging
+
+from . import __version__, curr_namespace, MoulinetteError
+from .extra.parameters import extraparameters_list
+
+## Extra parameters Parser
+
+class ExtraParser(object):
+    """
+    Global parser for the extra parameters.
+
+    """
+    def __init__(self, iface):
+        self.iface = iface
+        self.extra = OrderedDict()
+
+        # Append available extra parameters for the current interface
+        for klass in extraparameters_list:
+            if iface in klass.skipped_iface:
+                continue
+            self.extra[klass.name] = klass
+
+    def validate(self, arg_name, parameters):
+        """
+        Validate values of extra parameters for an argument
+
+        Keyword arguments:
+            - arg_name -- The argument name
+            - parameters -- A dict of extra parameters with their values
+
+        """
+        # Iterate over parameters to validate
+        for p, v in parameters.items():
+            # Remove unknow parameters
+            if p not in self.extra.keys():
+                del parameters[p]
+
+            # Validate parameter value
+            parameters[p] = self.extra[p].validate(v, arg_name)
+
+        return parameters
+
+    def parse(self, arg_name, arg_value, parameters):
+        """
+        Parse argument with extra parameters
+
+        Keyword arguments:
+            - arg_name -- The argument name
+            - arg_value -- The argument value
+            - parameters -- A dict of extra parameters with their values
+
+        """
+        # Iterate over available parameters
+        for p, klass in self.extra.items():
+            if p not in parameters.keys():
+                continue
+
+            # Initialize the extra parser
+            parser = klass(self.iface)
+
+            # Parse the argument
+            if isinstance(arg_value, list):
+                for v in arg_value:
+                    r = parser(parameters[p], arg_name, v)
+                    if r not in arg_value:
+                        arg_value.append(r)
+            else:
+                arg_value = parser(parameters[p], arg_name, arg_value)
+
+        return arg_value
+
+
+## Main class
+
+class ActionsMap(object):
+    """
+    Validate and process action defined into the actions map.
+
+    The actions map defines features and their usage of the main
+    application. It is composed by categories which contain one or more
+    action(s). Moreover, the action can have specific argument(s).
+
+    Keyword arguments:
+
+        - interface -- Interface type that requires the actions map.
+            Possible value is one of:
+                - 'cli' for the command line interface
+                - 'api' for an API usage (HTTP requests)
+
+        - use_cache -- False if it should parse the actions map file
+            instead of using the cached one.
+
+    """
+    def __init__(self, interface, use_cache=True):
+        self.use_cache = use_cache
+
+        try:
+            # Retrieve the interface parser
+            mod = __import__('interface.%s' % interface,
+                             globals=globals(), level=1,
+                             fromlist=['actionsmap_parser'])
+            parser = getattr(mod, 'actionsmap_parser')
+        except (AttributeError, ImportError):
+            raise MoulinetteError(22, _("Invalid interface '%s'" % interface))
+        else:
+            self._parser_class = parser
+
+        logging.debug("initializing ActionsMap for the '%s' interface" % interface)
+
+        actionsmaps = {}
+        namespaces = self.get_actionsmap_namespaces()
+        if curr_namespace and curr_namespace in namespaces:
+            namespaces = [curr_namespace]
+
+        # Iterate over actions map namespaces
+        for n in namespaces:
+            logging.debug("loading '%s' actions map namespace" % n)
+
+            if use_cache:
+                # Attempt to load cache if it exists
+                cache_file = '%s/%s.pkl' % (pkg.cachedir('actionsmap'), n)
+                if os.path.isfile(cache_file):
+                    with open(cache_file, 'r') as f:
+                        actionsmaps[n] = pickle.load(f)
+                else:
+                    self.use_cache = False
+                    actionsmaps = self.generate_cache(namespaces)
+                    break
+            else:
+                am_file = '%s/%s.yml' % (pkg.datadir('actionsmap'), n)
+                with open(am_file, 'r') as f:
+                    actionsmaps[n] = yaml.load(f)
+
+        # Generate parsers
+        self.extraparser = ExtraParser(interface)
+        self.parser = self._construct_parser(actionsmaps)
+
+    def process(self, args, **kwargs):
+        """
+        Parse arguments and process the proper action
+
+        Keyword arguments:
+            - args -- The arguments to parse
+            - **kwargs -- Additional interface arguments
+
+        """
+        # Parse arguments
+        arguments = vars(self.parser.parse_args(args, **kwargs))
+        arguments = self._parse_extra_parameters(arguments)
+
+        # Retrieve action information
+        namespace, category, action = arguments.pop('_info')
+        func_name = '%s_%s' % (category, action)
+
+        try:
+            mod = __import__('%s.%s' % (namespace, category),
+                             globals=globals(), level=0,
+                             fromlist=[func_name])
+            func = getattr(mod, func_name)
+        except (AttributeError, ImportError):
+            raise MoulinetteError(168, _('Function is not defined'))
+        else:
+            # Process the action
+            return func(**arguments)
+
+    @staticmethod
+    def get_actionsmap_namespaces():
+        """
+        Retrieve actions map namespaces from a given path
+
+        Returns:
+            A list of available namespaces
+
+        """
+        namespaces = []
+
+        for f in os.listdir(pkg.datadir('actionsmap')):
+            if f.endswith('.yml'):
+                namespaces.append(f[:-4])
+        return namespaces
+
+    @classmethod
+    def generate_cache(klass, namespaces=None):
+        """
+        Generate cache for the actions map's file(s)
+
+        Keyword arguments:
+            - namespaces -- A list of namespaces to generate cache for
+
+        Returns:
+            A dict of actions map for each namespaces
+
+        """
+        actionsmaps = {}
+        if not namespaces:
+            namespaces = klass.get_actionsmap_namespaces()
+
+        # Iterate over actions map namespaces
+        for n in namespaces:
+            logging.debug("generating cache for '%s' actions map namespace" % n)
+
+            # Read actions map from yaml file
+            am_file = pkg.datafile('actionsmap/%s.yml' % n)
+            with open(am_file, 'r') as f:
+                actionsmaps[n] = yaml.load(f)
+
+            # Cache actions map into pickle file
+            cache_file = pkg.cachefile('actionsmap/%s.pkl' % n, make_dir=True)
+            with open(cache_file, 'w') as f:
+                pickle.dump(actionsmaps[n], f)
+
+        return actionsmaps
+
+
+    ## Private class and methods
+
+    def _store_extra_parameters(self, parser, arg_name, arg_extra):
+        """
+        Store extra parameters for a given argument
+
+        Keyword arguments:
+            - parser -- Parser object for the arguments
+            - arg_name -- Argument name
+            - arg_extra -- Argument extra parameters
+
+        Returns:
+            The parser object
+
+        """
+        if arg_extra:
+            # Retrieve current extra parameters dict
+            extra = parser.get_default('_extra')
+            if not extra or not isinstance(extra, dict):
+                extra = {}
+
+            if not self.use_cache:
+                # Validate extra parameters for the argument
+                extra[arg_name] = self.extraparser.validate(arg_name, arg_extra)
+            else:
+                extra[arg_name] = arg_extra
+            parser.set_defaults(_extra=extra)
+
+        return parser
+
+    def _parse_extra_parameters(self, args):
+        """
+        Parse arguments with their extra parameters
+
+        Keyword arguments:
+            - args -- A dict of all arguments
+
+        Return:
+            The parsed arguments dict
+
+        """
+        # Retrieve extra parameters for the arguments
+        extra = args.pop('_extra', None)
+        if not extra:
+            return args
+
+        # Validate extra parameters for each arguments
+        for an, parameters in extra.items():
+            args[an] = self.extraparser.parse(an, args[an], parameters)
+
+        return args
+
+    def _construct_parser(self, actionsmaps):
+        """
+        Construct the parser with the actions map
+
+        Keyword arguments:
+            - actionsmaps -- A dict of multi-level dictionnary of
+                categories/actions/arguments list for each namespaces
+
+        Returns:
+            An interface relevant's parser object
+
+        """
+        # Instantiate parser
+        top_parser = self._parser_class()
+
+        # Iterate over actions map namespaces
+        for n, actionsmap in actionsmaps.items():
+            if 'general_arguments' in actionsmap:
+                # Parse general arguments
+                if top_parser.parse_general:
+                    parser = top_parser.add_general_parser()
+                    for an, ap in actionsmap['general_arguments'].items():
+                        if 'version' in ap:
+                            ap['version'] = ap['version'].replace('%version%',
+                                                                  __version__)
+                        argname = top_parser.format_arg_name(an, ap.pop('full', None))
+                        parser.add_argument(*argname, **ap)
+                del actionsmap['general_arguments']
+
+            # Parse categories
+            for cn, cp in actionsmap.items():
+                if 'actions' not in cp:
+                    continue
+                actions = cp.pop('actions')
+
+                # Add category parser
+                if top_parser.parse_category:
+                    cat_parser = top_parser.add_category_parser(cn, **cp)
+                else:
+                    cat_parser = top_parser
+
+                # Parse actions
+                if not top_parser.parse_action:
+                    continue
+                for an, ap in actions.items():
+                    arguments = ap.pop('arguments', {})
+
+                    # Add action parser
+                    parser = cat_parser.add_action_parser(an, **ap)
+                    if not parser:
+                        continue
+
+                    # Store action information
+                    parser.set_defaults(_info=(n, cn, an))
+
+                    # Add action arguments
+                    for argn, argp in arguments.items():
+                        name = top_parser.format_arg_name(argn, argp.pop('full', None))
+                        extra = argp.pop('extra', None)
+
+                        arg = parser.add_argument(*name, **argp)
+                        parser = self._store_extra_parameters(parser, arg.dest, extra)
+
+        return top_parser
diff --git a/src/moulinette/config.py b/src/moulinette/config.py
deleted file mode 100644
index 578c8ef1..00000000
--- a/src/moulinette/config.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# TODO: Remove permanent debug values
-import os
-
-# Path for the the web sessions
-session_path = '/var/cache/yunohost/session'
-
-# Path of the actions map definition(s)
-actionsmap_path =  os.path.dirname(__file__) +'/../../data/actionsmap'
-
-# Path for the actions map cache
-actionsmap_cache_path = '/var/cache/yunohost/actionsmap'
-
-# Path of the doc in json format
-doc_json_path = os.path.dirname(__file__) +'/../../doc'
diff --git a/src/moulinette/core.py b/src/moulinette/core.py
new file mode 100644
index 00000000..62fe34fe
--- /dev/null
+++ b/src/moulinette/core.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+import gettext
+from .helpers import colorize
+
+class Package(object):
+    """Package representation and easy access
+
+    Initialize directories and variables for the package and give them
+    easy access.
+
+    Keyword arguments:
+        - prefix -- The installation prefix
+        - libdir -- The library directory; usually, this would be
+                    prefix + '/lib' (or '/lib64') when installed
+        - cachedir -- The cache directory; usually, this would be
+                      '/var/cache' when installed
+        - destdir -- The destination prefix only if it's an installation
+
+    'prefix' and 'libdir' arguments should be empty in order to run
+    package from source.
+
+    """
+    def __init__(self, prefix, libdir, cachedir, destdir=None):
+        if not prefix and not libdir:
+            # Running from source directory
+            basedir = os.path.abspath(os.path.dirname(sys.argv[0]) +'/../')
+            self._datadir = os.path.join(basedir, 'data')
+            self._libdir = os.path.join(basedir, 'src')
+            self._cachedir = cachedir
+        else:
+            self._datadir = os.path.join(prefix, 'share/moulinette')
+            self._libdir = os.path.join(libdir, 'moulinette')
+            self._cachedir = os.path.join(cachedir, 'moulinette')
+
+            # Append library path to python's path
+            sys.path.append(self._libdir)
+        self._destdir = destdir or None
+
+
+    ## Easy access to directories and files
+
+    def datadir(self, subdir=None, **kwargs):
+        """Return the path to a data directory"""
+        return self.get_dir(self._datadir, subdir, **kwargs)
+
+    def datafile(self, filename, **kwargs):
+        """Return the path to a data file"""
+        return self.get_file(self._datadir, filename, **kwargs)
+
+    def libdir(self, subdir=None, **kwargs):
+        """Return the path to a lib directory"""
+        return self.get_dir(self._libdir, subdir, **kwargs)
+
+    def libfile(self, filename, **kwargs):
+        """Return the path to a lib file"""
+        return self.get_file(self._libdir, filename, **kwargs)
+
+    def cachedir(self, subdir=None, **kwargs):
+        """Return the path to a cache directory"""
+        return self.get_dir(self._cachedir, subdir, **kwargs)
+
+    def cachefile(self, filename, **kwargs):
+        """Return the path to a cache file"""
+        return self.get_file(self._cachedir, filename, **kwargs)
+
+
+    ## Standard methods
+
+    def get_dir(self, basedir, subdir=None, make_dir=False):
+        """Get a directory path
+
+        Return a path composed by a base directory and an optional
+        subdirectory. The path will be created if needed.
+
+        Keyword arguments:
+            - basedir -- The base directory
+            - subdir -- An optional subdirectory
+            - make_dir -- True if it should create needed directory
+
+        """
+        # Retrieve path
+        path = basedir
+        if self._destdir:
+            path = os.path.join(self._destdir, path)
+        if subdir:
+            path = os.path.join(path, subdir)
+
+        # Create directory
+        if make_dir and not os.path.isdir(path):
+            os.makedirs(path)
+        return path
+
+    def get_file(self, basedir, filename, **kwargs):
+        """Get a file path
+
+        Return the path of the filename in the specified directory. This
+        directory will be created if needed.
+
+        Keyword arguments:
+            - basedir -- The base directory of the file
+            - filename -- The filename or a path relative to basedir
+            - **kwargs -- Additional arguments for Package.get_dir
+
+        """
+        # Check for a directory in filename
+        subdir = os.path.dirname(filename) or None
+        if subdir:
+            filename = os.path.basename(filename)
+
+        # Get directory path
+        dirpath = self.get_dir(basedir, subdir, **kwargs)
+        return os.path.join(dirpath, filename)
+
+
+class MoulinetteError(Exception):
+    """Moulinette base exception
+
+    Keyword arguments:
+        - code -- Integer error code
+        - message -- Error message to display
+
+    """
+    def __init__(self, code, message):
+        self.code = code
+        self.message = message
+
+        errorcode_desc = {
+            1   : _('Fail'),
+            13  : _('Permission denied'),
+            17  : _('Already exists'),
+            22  : _('Invalid arguments'),
+            87  : _('Too many users'),
+            111 : _('Connection refused'),
+            122 : _('Quota exceeded'),
+            125 : _('Operation canceled'),
+            167 : _('Not found'),
+            168 : _('Undefined'),
+            169 : _('LDAP operation error')
+        }
+        if code in errorcode_desc:
+            self.desc = errorcode_desc[code]
+        else:
+            self.desc = _('Error %s' % code)
+
+    def __str__(self, colorized=False):
+        desc = self.desc
+        if colorized:
+            desc = colorize(self.desc, 'red')
+        return _('%s: %s' % (desc, self.message))
+
+    def colorize(self):
+        return self.__str__(colorized=True)
diff --git a/src/moulinette/core/actionsmap.py b/src/moulinette/core/actionsmap.py
deleted file mode 100644
index 8c90d973..00000000
--- a/src/moulinette/core/actionsmap.py
+++ /dev/null
@@ -1,503 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import argparse
-import pickle
-import yaml
-import re
-import os
-from collections import OrderedDict
-
-import logging
-
-from .. import __version__
-from ..config import actionsmap_path, actionsmap_cache_path
-
-from extraparameters import extraparameters_list
-from helpers import Interface, YunoHostError
-
-## Additional parsers
-
-class _HTTPArgumentParser(object):
-
-    def __init__(self, method, uri):
-        # Initialize the ArgumentParser object
-        self._parser = argparse.ArgumentParser(usage='',
-                                               prefix_chars='@',
-                                               add_help=False)
-        self._parser.error = self._error
-
-        self.method = method
-        self.uri = uri
-
-        self._positional = []   # list(arg_name)
-        self._optional = {}     # dict({arg_name: option_strings})
-
-    def set_defaults(self, **kwargs):
-        return self._parser.set_defaults(**kwargs)
-
-    def get_default(self, dest):
-        return self._parser.get_default(dest)
-
-    def add_argument(self, *args, **kwargs):
-        action = self._parser.add_argument(*args, **kwargs)
-
-        # Append newly created action
-        if len(action.option_strings) == 0:
-            self._positional.append(action.dest)
-        else:
-            self._optional[action.dest] = action.option_strings
-
-        return action
-
-    def parse_args(self, args):
-        arg_strings = []
-
-        ## Append an argument to the current one
-        def append(arg_strings, value, option_string=None):
-            # TODO: Process list arguments
-            if isinstance(value, bool):
-                # Append the option string only
-                if option_string is not None:
-                    arg_strings.append(option_string)
-            elif isinstance(value, str):
-                if option_string is not None:
-                    arg_strings.append(option_string)
-                    arg_strings.append(value)
-                else:
-                    arg_strings.append(value)
-
-            return arg_strings
-
-        # Iterate over positional arguments
-        for dest in self._positional:
-            if dest in args:
-                arg_strings = append(arg_strings, args[dest])
-
-        # Iterate over optional arguments
-        for dest, opt in self._optional.items():
-            if dest in args:
-                arg_strings = append(arg_strings, args[dest], opt[0])
-        return self._parser.parse_args(arg_strings)
-
-    def _error(self, message):
-        # TODO: Raise a proper exception
-        raise Exception(message)
-
-class HTTPParser(object):
-    """
-    Object for parsing HTTP requests into Python objects.
-
-    """
-
-    def __init__(self):
-        self._parsers = {}   # dict({(method, uri): _HTTPArgumentParser})
-
-    @property
-    def routes(self):
-        """Get current routes"""
-        return self._parsers.keys()
-
-    def add_parser(self, method, uri):
-        """
-        Add a parser for a given route
-
-        Keyword arguments:
-            - method -- The route's HTTP method (GET, POST, PUT, DELETE)
-            - uri -- The route's URI
-
-        Returns:
-            A new _HTTPArgumentParser object for the route
-
-        """
-        # Check if a parser already exists for the route
-        key = (method, uri)
-        if key in self.routes:
-            raise ValueError("A parser for '%s' already exists" % key)
-
-        # Create and append parser
-        parser = _HTTPArgumentParser(method, uri)
-        self._parsers[key] = parser
-
-        # Return the created parser
-        return parser
-
-    def parse_args(self, method, uri, args={}):
-        """
-        Convert argument variables to objects and assign them as
-        attributes of the namespace for a given route
-
-        Keyword arguments:
-            - method -- The route's HTTP method (GET, POST, PUT, DELETE)
-            - uri -- The route's URI
-            - args -- Argument variables for the route
-
-        Returns:
-            The populated namespace
-
-        """
-        # Retrieve the parser for the route
-        key = (method, uri)
-        if key not in self.routes:
-            raise ValueError("No parser for '%s %s' found" % key)
-
-        return self._parsers[key].parse_args(args)
-
-class ExtraParser(object):
-    """
-    Global parser for the extra parameters.
-
-    """
-    def __init__(self, iface):
-        self.iface = iface
-        self.extra = OrderedDict()
-
-        # Append available extra parameters for the current interface
-        for klass in extraparameters_list:
-            if iface in klass.skipped_iface:
-                continue
-            if klass.name in self.extra:
-                logging.warning("extra parameter named '%s' was already added" % klass.name)
-                continue
-            self.extra[klass.name] = klass
-
-    def validate(self, arg_name, parameters):
-        """
-        Validate values of extra parameters for an argument
-
-        Keyword arguments:
-            - arg_name -- The argument name
-            - parameters -- A dict of extra parameters with their values
-
-        """
-        # Iterate over parameters to validate
-        for p, v in parameters.items():
-            # Remove unknow parameters
-            if p not in self.extra.keys():
-                del parameters[p]
-
-            # Validate parameter value
-            parameters[p] = self.extra[p].validate(v, arg_name)
-
-        return parameters
-
-    def parse(self, arg_name, arg_value, parameters):
-        """
-        Parse argument with extra parameters
-
-        Keyword arguments:
-            - arg_name -- The argument name
-            - arg_value -- The argument value
-            - parameters -- A dict of extra parameters with their values
-
-        """
-        # Iterate over available parameters
-        for p, klass in self.extra.items():
-            if p not in parameters.keys():
-                continue
-
-            # Initialize the extra parser
-            parser = klass(self.iface)
-
-            # Parse the argument
-            if isinstance(arg_value, list):
-                for v in arg_value:
-                    r = parser(parameters[p], arg_name, v)
-                    if r not in arg_value:
-                        arg_value.append(r)
-            else:
-                arg_value = parser(parameters[p], arg_name, arg_value)
-
-        return arg_value
-
-
-## Main class
-
-class ActionsMap(object):
-    """
-    Validate and process action defined into the actions map.
-
-    The actions map defines features and their usage of the main
-    application. It is composed by categories which contain one or more
-    action(s). Moreover, the action can have specific argument(s).
-
-    Keyword arguments:
-
-        - interface -- Interface type that requires the actions map.
-            Possible value is one of:
-                - 'cli' for the command line interface
-                - 'api' for an API usage (HTTP requests)
-
-        - use_cache -- False if it should parse the actions map file
-            instead of using the cached one.
-
-    """
-    def __init__(self, interface, use_cache=True):
-        if interface not in Interface.all():
-            raise ValueError(_("Invalid interface '%s'" % interface))
-        self.interface = interface
-        self.use_cache = use_cache
-
-        logging.debug("initializing ActionsMap for the '%s' interface" % interface)
-
-        # Iterate over actions map namespaces
-        actionsmaps = {}
-        for n in self.get_actionsmap_namespaces():
-            logging.debug("loading '%s' actions map namespace" % n)
-
-            if use_cache:
-                # Attempt to load cache if it exists
-                cache_file = '%s/%s.pkl' % (actionsmap_cache_path, n)
-                if os.path.isfile(cache_file):
-                    with open(cache_file, 'r') as f:
-                        actionsmaps[n] = pickle.load(f)
-                else:
-                    self.use_cache = False
-                    actionsmaps = self.generate_cache()
-                    break
-            else:
-                am_file = '%s/%s.yml' % (actionsmap_path, n)
-                with open(am_file, 'r') as f:
-                    actionsmaps[n] = yaml.load(f)
-
-        # Generate parsers
-        self.extraparser = ExtraParser(interface)
-        self.parser = self._construct_parser(actionsmaps)
-
-    def process(self, args, route=None):
-        """
-        Parse arguments and process the proper action
-
-        Keyword arguments:
-            - args -- The arguments to parse
-            - route -- A tupple (method, uri) of the requested route (api only)
-
-        """
-        arguments = None
-
-        # Parse arguments
-        if self.interface == Interface.cli:
-            arguments = self.parser.parse_args(args)
-        elif self.interface == Interface.api:
-            if route is None:
-                # TODO: Raise a proper exception
-                raise Exception(_("Missing route argument"))
-            method, uri = route
-            arguments = self.parser.parse_args(method, uri, args)
-        arguments = vars(arguments)
-
-        # Parse extra parameters
-        arguments = self._parse_extra_parameters(arguments)
-
-        # Retrieve action information
-        namespace = arguments['_info']['namespace']
-        category = arguments['_info']['category']
-        action = arguments['_info']['action']
-        del arguments['_info']
-
-        module = '%s.%s' % (namespace, category)
-        function = '%s_%s' % (category, action)
-
-        try:
-            mod = __import__(module, globals=globals(), fromlist=[function], level=0)
-            func = getattr(mod, function)
-        except (AttributeError, ImportError):
-            raise YunoHostError(168, _('Function is not defined'))
-        else:
-            # Process the action
-            return func(**arguments)
-
-    @staticmethod
-    def get_actionsmap_namespaces(path=actionsmap_path):
-        """
-        Retrieve actions map namespaces from a given path
-
-        Returns:
-            A list of available namespaces
-
-        """
-        namespaces = []
-
-        for f in os.listdir(path):
-            if f.endswith('.yml'):
-                namespaces.append(f[:-4])
-        return namespaces
-
-    @classmethod
-    def generate_cache(klass):
-        """
-        Generate cache for the actions map's file(s)
-
-        Returns:
-            A dict of actions map for each namespaces
-
-        """
-        actionsmaps = {}
-
-        if not os.path.isdir(actionsmap_cache_path):
-            os.makedirs(actionsmap_cache_path)
-
-        # Iterate over actions map namespaces
-        for n in klass.get_actionsmap_namespaces():
-            logging.debug("generating cache for '%s' actions map namespace" % n)
-
-            # Read actions map from yaml file
-            am_file = '%s/%s.yml' % (actionsmap_path, n)
-            with open(am_file, 'r') as f:
-                actionsmaps[n] = yaml.load(f)
-
-            # Cache actions map into pickle file
-            cache_file = '%s/%s.pkl' % (actionsmap_cache_path, n)
-            with open(cache_file, 'w') as f:
-                pickle.dump(actionsmaps[n], f)
-
-        return actionsmaps
-
-
-    ## Private class and methods
-
-    def _store_extra_parameters(self, parser, arg_name, arg_params):
-        """
-        Store extra parameters for a given argument
-
-        Keyword arguments:
-            - parser -- Parser object for the arguments
-            - arg_name -- Argument name
-            - arg_params -- Argument parameters
-
-        Returns:
-            The parser object
-
-        """
-        if 'extra' in arg_params:
-            # Retrieve current extra parameters dict
-            extra = parser.get_default('_extra')
-            if not extra or not isinstance(extra, dict):
-                extra = {}
-
-            if not self.use_cache:
-                # Validate extra parameters for the argument
-                extra[arg_name] = self.extraparser.validate(arg_name, arg_params['extra'])
-            else:
-                extra[arg_name] = arg_params['extra']
-            parser.set_defaults(_extra=extra)
-
-        return parser
-
-    def _parse_extra_parameters(self, args):
-        """
-        Parse arguments with their extra parameters
-
-        Keyword arguments:
-            - args -- A dict of all arguments
-
-        Return:
-            The parsed arguments dict
-
-        """
-        # Retrieve extra parameters from the arguments
-        if '_extra' not in args:
-            return args
-        extra = args['_extra']
-        del args['_extra']
-
-        # Validate extra parameters for each arguments
-        for an, parameters in extra.items():
-            args[an] = self.extraparser.parse(an, args[an], parameters)
-
-        return args
-
-    def _construct_parser(self, actionsmaps):
-        """
-        Construct the parser with the actions map
-
-        Keyword arguments:
-            - actionsmaps -- A dict of multi-level dictionnary of
-                categories/actions/arguments list for each namespaces
-
-        Returns:
-            An interface relevant's parser object
-
-        """
-        top_parser = None
-        iface = self.interface
-
-        # Create parser object
-        if iface == Interface.cli:
-            # TODO: Add descritpion (from __description__?)
-            top_parser = argparse.ArgumentParser()
-            top_subparsers = top_parser.add_subparsers()
-        elif iface == Interface.api:
-            top_parser = HTTPParser()
-
-        ## Format option strings from argument parameters
-        def _option_strings(arg_name, arg_params):
-            if iface == Interface.cli:
-                if arg_name[0] == '-' and 'full' in arg_params:
-                    return [arg_name, arg_params['full']]
-                return [arg_name]
-            elif iface == Interface.api:
-                if arg_name[0] != '-':
-                    return [arg_name]
-                if 'full' in arg_params:
-                    return [arg_params['full'].replace('--', '@', 1)]
-                if arg_name.startswith('--'):
-                    return [arg_name.replace('--', '@', 1)]
-                return [arg_name.replace('-', '@', 1)]
-
-        ## Remove extra parameters
-        def _clean_params(arg_params):
-            for k in {'full', 'extra'}:
-                if k in arg_params:
-                    del arg_params[k]
-            return arg_params
-
-        # Iterate over actions map namespaces
-        for n, actionsmap in actionsmaps.items():
-            # Parse general arguments for the cli only
-            if iface == Interface.cli:
-                for an, ap in actionsmap['general_arguments'].items():
-                    if 'version' in ap:
-                        ap['version'] = ap['version'].replace('%version%', __version__)
-                    top_parser.add_argument(*_option_strings(an, ap), **_clean_params(ap))
-            del actionsmap['general_arguments']
-
-            # Parse categories
-            for cn, cp in actionsmap.items():
-                if 'actions' not in cp:
-                    continue
-
-                # Add category subparsers for the cli only
-                if iface == Interface.cli:
-                    c_help = cp.get('category_help')
-                    subparsers = top_subparsers.add_parser(cn, help=c_help).add_subparsers()
-
-                # Parse actions
-                for an, ap in cp['actions'].items():
-                    parser = None
-
-                    # Add parser for the current action
-                    if iface == Interface.cli:
-                        a_help = ap.get('action_help')
-                        parser = subparsers.add_parser(an, help=a_help)
-                    elif iface == Interface.api and 'api' in ap:
-                        # Extract method and uri
-                        m = re.match('(GET|POST|PUT|DELETE) (/\S+)', ap['api'])
-                        if m:
-                            parser = top_parser.add_parser(m.group(1), m.group(2))
-                    if not parser:
-                        continue
-
-                    # Store action information
-                    parser.set_defaults(_info={'namespace': n,
-                                               'category': cn,
-                                               'action': an})
-
-                    # Add arguments
-                    if not 'arguments' in ap:
-                        continue
-                    for argn, argp in ap['arguments'].items():
-                        arg = parser.add_argument(*_option_strings(argn, argp),
-                                                  **_clean_params(argp.copy()))
-                        parser = self._store_extra_parameters(parser, arg.dest, argp)
-
-        return top_parser
diff --git a/src/moulinette/core/__init__.py b/src/moulinette/extra/__init__.py
old mode 100755
new mode 100644
similarity index 100%
rename from src/moulinette/core/__init__.py
rename to src/moulinette/extra/__init__.py
diff --git a/src/moulinette/core/extraparameters.py b/src/moulinette/extra/parameters.py
similarity index 95%
rename from src/moulinette/core/extraparameters.py
rename to src/moulinette/extra/parameters.py
index 3f4248c0..f1bf0ba9 100644
--- a/src/moulinette/core/extraparameters.py
+++ b/src/moulinette/extra/parameters.py
@@ -4,7 +4,8 @@ import getpass
 import re
 import logging
 
-from helpers import Interface, colorize, YunoHostError
+from .. import MoulinetteError
+from ..helpers import colorize
 
 class _ExtraParameter(object):
     """
@@ -78,7 +79,7 @@ class AskParameter(_ExtraParameter):
 
     """
     name = 'ask'
-    skipped_iface = {Interface.api}
+    skipped_iface = { 'api' }
 
     def __call__(self, message, arg_name, arg_value):
         # TODO: Fix asked arguments ordering
@@ -119,7 +120,7 @@ class PasswordParameter(AskParameter):
         pwd1 = getpass.getpass(colorize(message + ': ', 'cyan'))
         pwd2 = getpass.getpass(colorize('Retype ' + message + ': ', 'cyan'))
         if pwd1 != pwd2:
-            raise YunoHostError(22, _("Passwords don't match"))
+            raise MoulinetteError(22, _("Passwords don't match"))
         return pwd1
 
 class PatternParameter(_ExtraParameter):
@@ -137,7 +138,7 @@ class PatternParameter(_ExtraParameter):
         message = arguments[1]
 
         if arg_value is not None and not re.match(pattern, arg_value):
-            raise YunoHostError(22, message)
+            raise MoulinetteError(22, message)
         return arg_value
 
     @staticmethod
diff --git a/src/moulinette/core/helpers.py b/src/moulinette/helpers.py
similarity index 63%
rename from src/moulinette/core/helpers.py
rename to src/moulinette/helpers.py
index 3d0517aa..69efaf1f 100644
--- a/src/moulinette/core/helpers.py
+++ b/src/moulinette/helpers.py
@@ -21,21 +21,6 @@ import getpass
 if not __debug__:
     import traceback
 
-
-class Interface():
-    """
-    Contain available interfaces to use with the moulinette.
-
-    """
-    api = 'api'
-    cli = 'cli'
-
-    @classmethod
-    def all(klass):
-        """Get a list of all interfaces"""
-        ifaces = set(i for i in dir(klass) if not i.startswith('_'))
-        return ifaces
-
 win = []
 
 def random_password(length=8):
@@ -105,33 +90,6 @@ def win_msg(astr):
     win.append(astr)
 
 
-def str_to_func(astr):
-    """
-    Call a function from a string name
-
-    Keyword arguments:
-        astr -- Name of function to call
-
-    Returns:
-        Function
-
-    """
-    try:
-        module, _, function = astr.rpartition('.')
-        if module:
-            __import__(module)
-            mod = sys.modules[module]
-        else:
-            mod = sys.modules['__main__']  # default module
-
-        func = getattr(mod, function)
-    except (AttributeError, ImportError):
-        #raise YunoHostError(168, _('Function is not defined'))
-        return None
-    else:
-        return func
-
-
 def validate(pattern, array):
     """
     Validate attributes with a pattern
@@ -441,115 +399,3 @@ class YunoHostLDAP(Singleton):
             else:
                 raise YunoHostError(17, _('Attribute already exists') + ' "' + attr + '=' + value + '"')
         return True
-
-
-def parse_dict(action_map):
-    """
-    Turn action dictionnary to parser, subparsers and arguments
-
-    Keyword arguments:
-        action_map -- Multi-level dictionnary of categories/actions/arguments list
-
-    Returns:
-        Namespace of args
-
-    """
-    # Intialize parsers
-    parsers = subparsers_category = subparsers_action = {}
-    parsers['general'] = argparse.ArgumentParser()
-    subparsers = parsers['general'].add_subparsers()
-    new_args = []
-    patterns = {}
-
-    # Add general arguments
-    for arg_name, arg_params in action_map['general_arguments'].items():
-        if 'version' in arg_params:
-            v = arg_params['version']
-            arg_params['version'] = v.replace('%version%', __version__)
-        if 'full' in arg_params:
-            arg_names = [arg_name, arg_params['full']]
-            arg_fullname = arg_params['full']
-            del arg_params['full']
-        else: arg_names = [arg_name]
-        parsers['general'].add_argument(*arg_names, **arg_params)
-
-    del action_map['general_arguments']
-
-    # Split categories into subparsers
-    for category, category_params in action_map.items():
-        if 'category_help' not in category_params: category_params['category_help'] = ''
-        subparsers_category[category] = subparsers.add_parser(category, help=category_params['category_help'])
-        subparsers_action[category] = subparsers_category[category].add_subparsers()
-        # Split actions
-        if 'actions' in category_params:
-            for action, action_params in category_params['actions'].items():
-                if 'action_help' not in action_params: action_params['action_help'] = ''
-                parsers[category + '_' + action] = subparsers_action[category].add_parser(action, help=action_params['action_help'])
-                # Set the action s related function
-                parsers[category + '_' + action].set_defaults(
-                    func=str_to_func('yunohost_' + category + '.'
-                                     + category + '_' + action.replace('-', '_')))
-                # Add arguments
-                if 'arguments' in action_params:
-                    for arg_name, arg_params in action_params['arguments'].items():
-                        arg_fullname = False
-
-                        if 'password' in arg_params:
-                            if arg_params['password']: is_password = True
-                            del arg_params['password']
-                        else: is_password = False
-
-                        if 'full' in arg_params:
-                            arg_names = [arg_name, arg_params['full']]
-                            arg_fullname = arg_params['full']
-                            del arg_params['full']
-                        else: arg_names = [arg_name]
-
-                        if 'ask' in arg_params:
-                            require_input = True
-                            if '-h' in sys.argv or '--help' in sys.argv:
-                                require_input = False
-                            if (category != sys.argv[1]) or (action != sys.argv[2]):
-                                require_input = False
-                            for name in arg_names:
-                                if name in sys.argv[2:]: require_input = False
-
-                            if require_input:
-                                if is_password:
-                                    if os.isatty(1):
-                                        pwd1 = getpass.getpass(colorize(arg_params['ask'] + ': ', 'cyan'))
-                                        pwd2 = getpass.getpass(colorize('Retype ' + arg_params['ask'][0].lower() + arg_params['ask'][1:] + ': ', 'cyan'))
-                                        if pwd1 != pwd2:
-                                            raise YunoHostError(22, _("Passwords don't match"))
-                                            sys.exit(1)
-                                    else:
-                                        raise YunoHostError(22, _("Missing arguments") + ': ' + arg_name)
-                                    if arg_name[0] == '-': arg_extend = [arg_name, pwd1]
-                                    else: arg_extend = [pwd1]
-                                else:
-                                    if os.isatty(1):
-                                        arg_value = raw_input(colorize(arg_params['ask'] + ': ', 'cyan'))
-                                    else:
-                                        raise YunoHostError(22, _("Missing arguments") + ': ' + arg_name)
-                                    if arg_name[0] == '-': arg_extend = [arg_name, arg_value]
-                                    else: arg_extend = [arg_value]
-                                new_args.extend(arg_extend)
-                            del arg_params['ask']
-
-                        if 'pattern' in arg_params:
-                            if (category == sys.argv[1]) and (action == sys.argv[2]):
-                                if 'dest' in arg_params: name = arg_params['dest']
-                                elif arg_fullname: name = arg_fullname[2:]
-                                else: name = arg_name
-                                name = name.replace('-', '_')
-                                patterns[name] = arg_params['pattern']
-                            del arg_params['pattern']
-
-                        parsers[category + '_' + action].add_argument(*arg_names, **arg_params)
-
-    args = parsers['general'].parse_args(sys.argv.extend(new_args))
-    args_dict = vars(args)
-    for key, value in patterns.items():
-        validate(value, args_dict[key])
-
-    return args
diff --git a/src/moulinette/interface/__init__.py b/src/moulinette/interface/__init__.py
new file mode 100755
index 00000000..3a99535f
--- /dev/null
+++ b/src/moulinette/interface/__init__.py
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+
+class BaseParser(object):
+    """Actions map's base Parser
+
+    Each interfaces must implement a parser class derived from this
+    class. It is used to parse the main parts of the actions map (i.e.
+    general arguments, categories and actions).
+
+    """
+
+    ## Optional variables
+    # Each parser classes can overwrite these variables.
+
+    """Either it will parse general arguments, or not"""
+    parse_general = True
+
+    """Either it will parse categories, or not"""
+    parse_category = True
+
+    """Either it will parse actions, or not"""
+    parse_action = True
+
+
+    ## Virtual methods
+    # Each parser classes can implement these methods.
+
+    @staticmethod
+    def format_arg_name(name, full):
+        """Format argument name
+
+        Format agument name depending on its 'full' parameters and return
+        a list to use it as option string for the argument parser.
+
+        Keyword arguments:
+            - name -- The argument name
+            - full -- The argument's 'full' parameter
+
+        Returns:
+            A list of option strings
+
+        """
+        raise NotImplementedError("derived class '%s' must override this method" % \
+                                    self.__class__.__name__)
+
+    def add_general_parser(self, **kwargs):
+        """Add a parser for general arguments
+
+        Create and return an argument parser for general arguments.
+
+        Returns:
+            An ArgumentParser based object
+
+        """
+        if not self.parse_general:
+            msg = "doesn't parse general arguments"
+        else:
+            msg = "must override this method"
+        raise NotImplementedError("derived class '%s' %s" % \
+                                    (self.__class__.__name__, msg))
+
+    def add_category_parser(self, name, **kwargs):
+        """Add a parser for a category
+
+        Create a new category and return a parser for it.
+
+        Keyword arguments:
+            - name -- The category name
+
+        Returns:
+            A BaseParser based object
+
+        """
+        if not self.parse_categories:
+            msg = "doesn't parse categories"
+        else:
+            msg = "must override this method"
+        raise NotImplementedError("derived class '%s' %s" % \
+                                    (self.__class__.__name__, msg))
+
+    def add_action_parser(self, name, **kwargs):
+        """Add a parser for an action
+
+        Create a new action and return an argument parser for it.
+
+        Keyword arguments:
+            - name -- The action name
+
+        Returns:
+            An ArgumentParser based object
+
+        """
+        if not self.parse_general:
+            msg = "doesn't parse actions"
+        else:
+            msg = "must override this method"
+        raise NotImplementedError("derived class '%s' %s" % \
+                                    (self.__class__.__name__, msg))
+
+    def parse_args(self, args, **kwargs):
+        """Parse arguments
+
+        Convert argument variables to objects and assign them as
+        attributes of the namespace.
+
+        Keyword arguments:
+            - args -- Arguments string or dict (TODO)
+
+        Returns:
+            The populated namespace
+
+        """
+        raise NotImplementedError("derived class '%s' must override this method" % \
+                                    self.__class__.__name__)
diff --git a/src/moulinette/core/api.py b/src/moulinette/interface/api.py
similarity index 53%
rename from src/moulinette/core/api.py
rename to src/moulinette/interface/api.py
index 19e0af18..6c959c9f 100644
--- a/src/moulinette/core/api.py
+++ b/src/moulinette/interface/api.py
@@ -1,16 +1,162 @@
 # -*- coding: utf-8 -*-
 
+import re
+import argparse
 import os.path
 from bottle import Bottle, request, response, HTTPResponse
 from beaker.middleware import SessionMiddleware
 
-from ..config import session_path, doc_json_path
-from helpers import YunoHostError, YunoHostLDAP
+from . import BaseParser
+from .. import MoulinetteError
+from ..helpers import YunoHostError, YunoHostLDAP
+
+## API arguments Parser
+
+class _HTTPArgumentParser(object):
+    """Argument parser for HTTP requests
+
+    Object for parsing HTTP requests into Python objects. It is based
+    on argparse.ArgumentParser class and implements some of its methods.
+
+    """
+    def __init__(self):
+        # Initialize the ArgumentParser object
+        self._parser = argparse.ArgumentParser(usage='',
+                                               prefix_chars='@',
+                                               add_help=False)
+        self._parser.error = self._error
+
+        self._positional = []   # list(arg_name)
+        self._optional = {}     # dict({arg_name: option_strings})
+
+    def set_defaults(self, **kwargs):
+        return self._parser.set_defaults(**kwargs)
+
+    def get_default(self, dest):
+        return self._parser.get_default(dest)
+
+    def add_argument(self, *args, **kwargs):
+        action = self._parser.add_argument(*args, **kwargs)
+
+        # Append newly created action
+        if len(action.option_strings) == 0:
+            self._positional.append(action.dest)
+        else:
+            self._optional[action.dest] = action.option_strings
+
+        return action
+
+    def parse_args(self, args):
+        arg_strings = []
+
+        ## Append an argument to the current one
+        def append(arg_strings, value, option_string=None):
+            # TODO: Process list arguments
+            if isinstance(value, bool):
+                # Append the option string only
+                if option_string is not None:
+                    arg_strings.append(option_string)
+            elif isinstance(value, str):
+                if option_string is not None:
+                    arg_strings.append(option_string)
+                    arg_strings.append(value)
+                else:
+                    arg_strings.append(value)
+
+            return arg_strings
+
+        # Iterate over positional arguments
+        for dest in self._positional:
+            if dest in args:
+                arg_strings = append(arg_strings, args[dest])
+
+        # Iterate over optional arguments
+        for dest, opt in self._optional.items():
+            if dest in args:
+                arg_strings = append(arg_strings, args[dest], opt[0])
+        return self._parser.parse_args(arg_strings)
+
+    def _error(self, message):
+        # TODO: Raise a proper exception
+        raise MoulinetteError(1, message)
+
+class APIParser(BaseParser):
+    """Actions map's API Parser
+
+    """
+    parse_category = False
+    parse_general = False
+
+    def __init__(self):
+        self._parsers = {}   # dict({(method, path): _HTTPArgumentParser})
+
+    @property
+    def routes(self):
+        """Get current routes"""
+        return self._parsers.keys()
+
+    
+    ## Implement virtual methods
+
+    @staticmethod
+    def format_arg_name(name, full):
+        if name[0] != '-':
+            return [name]
+        if full:
+            return [full.replace('--', '@', 1)]
+        if name.startswith('--'):
+            return [name.replace('--', '@', 1)]
+        return [name.replace('-', '@', 1)]
+
+    def add_action_parser(self, name, api=None, **kwargs):
+        """Add a parser for an action
+
+        Keyword arguments:
+            - api -- The action route (e.g. 'GET /' )
+
+        Returns:
+            A new _HTTPArgumentParser object for the route
+
+        """
+        if not api:
+            return None
+
+        # Validate action route
+        m = re.match('(GET|POST|PUT|DELETE) (/\S+)', api)
+        if not m:
+            return None
+
+        # Check if a parser already exists for the route
+        key = (m.group(1), m.group(2))
+        if key in self.routes:
+            raise ValueError("A parser for '%s' already exists" % key)
+
+        # Create and append parser
+        parser = _HTTPArgumentParser()
+        self._parsers[key] = parser
+
+        # Return the created parser
+        return parser
+
+    def parse_args(self, args, route, **kwargs):
+        """Parse arguments
+
+        Keyword arguments:
+            - route -- The action route (e.g. 'GET /' )
+
+        """
+        # Retrieve the parser for the route
+        if route not in self.routes:
+            raise MoulinetteError(22, "No parser for '%s %s' found" % key)
+
+        return self._parsers[route].parse_args(args)
+
+actionsmap_parser = APIParser
 
 
-## Bottle Plugins
+## API moulinette interface
 
-class APIAuthPlugin(object):
+class _APIAuthPlugin(object):
     """
     Manage the authentication for the API access.
 
@@ -20,8 +166,7 @@ class APIAuthPlugin(object):
 
     def __init__(self):
         # TODO: Add options (e.g. session type, content type, ...)
-        if not os.path.isdir(session_path):
-            os.makedirs(session_path)
+        pass
 
     @property
     def app(self):
@@ -44,7 +189,7 @@ class APIAuthPlugin(object):
         session_opts = {
             'session.type': 'file',
             'session.cookie_expires': True,
-            'session.data_dir': session_path,
+            'session.data_dir': pkg.cachedir('session', make_dir=True),
             'session.secure': True
         }
         self._app = SessionMiddleware(app, session_opts)
@@ -119,7 +264,7 @@ class APIAuthPlugin(object):
             return True
         return False
 
-class ActionsMapPlugin(object):
+class _ActionsMapPlugin(object):
     """
     Process action for the request using the actions map.
 
@@ -158,19 +303,15 @@ class ActionsMapPlugin(object):
         return wrapper
 
 
-## Main class
-
 class MoulinetteAPI(object):
     """
     Initialize a HTTP server which serves the API to access to the
     moulinette actions.
 
     Keyword arguments:
-
         - actionsmap -- The relevant ActionsMap instance
-
         - routes -- A dict of additional routes to add in the form of
-            {(method, uri): callback}
+            {(method, path): callback}
 
     """
 
@@ -182,14 +323,14 @@ class MoulinetteAPI(object):
                   callback=self.doc, skip=['apiauth'])
 
         # Append routes from the actions map
-        amap = ActionsMapPlugin(actionsmap)
-        for (m, u) in actionsmap.parser.routes:
-            app.route(u, method=m, callback=self._error, apply=amap)
+        amap = _ActionsMapPlugin(actionsmap)
+        for (m, p) in actionsmap.parser.routes:
+            app.route(p, method=m, callback=self._error, apply=amap)
 
         # Append additional routes
         # TODO: Add an option to skip auth for the route
-        for (m, u), c in routes.items():
-            app.route(u, method=m, callback=c)
+        for (m, p), c in routes.items():
+            app.route(p, method=m, callback=c)
 
         # Define and install a plugin which sets proper header
         def apiheader(callback):
@@ -201,7 +342,7 @@ class MoulinetteAPI(object):
         app.install(apiheader)
 
         # Install authentication plugin
-        apiauth = APIAuthPlugin()
+        apiauth = _APIAuthPlugin()
         app.install(apiauth)
 
         self._app = apiauth.app
@@ -220,11 +361,11 @@ class MoulinetteAPI(object):
 
         """
         if category is None:
-            with open(doc_json_path +'/resources.json') as f:
+            with open(pkg.datafile('doc/resources.json')) as f:
                 return f.read()
 
         try:
-            with open(doc_json_path +'/'+ category +'.json') as f:
+            with open(pkg.datafile('doc/%s.json' % category)) as f:
                 return f.read()
         except IOError:
             return 'unknown'
diff --git a/src/moulinette/interface/cli.py b/src/moulinette/interface/cli.py
new file mode 100644
index 00000000..c4be4c57
--- /dev/null
+++ b/src/moulinette/interface/cli.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+
+import argparse
+from . import BaseParser
+
+## CLI arguments Parser
+
+class CLIParser(BaseParser):
+    """Actions map's CLI Parser
+
+    """
+    def __init__(self, parser=None):
+        self._parser = parser or argparse.ArgumentParser()
+        self._subparsers = self._parser.add_subparsers()
+
+    @staticmethod
+    def format_arg_name(name, full):
+        if name[0] == '-' and full:
+            return [name, full]
+        return [name]
+
+    def add_general_parser(self, **kwargs):
+        return self._parser
+
+    def add_category_parser(self, name, category_help=None, **kwargs):
+        parser = self._subparsers.add_parser(name, help=category_help)
+        return CLIParser(parser)
+
+    def add_action_parser(self, name, action_help, **kwargs):
+        return self._subparsers.add_parser(name, help=action_help)
+
+    def parse_args(self, args, **kwargs):
+        return self._parser.parse_args(args)
+
+actionsmap_parser = CLIParser
diff --git a/src/yunohost/app.py b/src/yunohost/app.py
index 03a6cd77..eeb9808b 100644
--- a/src/yunohost/app.py
+++ b/src/yunohost/app.py
@@ -37,7 +37,7 @@ from domain import domain_list, domain_add
 from user import user_info, user_list
 from hook import hook_exec, hook_add, hook_remove
 
-from .moulinette.core.helpers import YunoHostError, YunoHostLDAP, win_msg, random_password, is_true, validate
+from moulinette.helpers import YunoHostError, YunoHostLDAP, win_msg, random_password, is_true, validate
 
 repo_path        = '/var/cache/yunohost/repo'
 apps_path        = '/usr/share/yunohost/apps'
diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py
index 0e245a62..d4723d9d 100644
--- a/src/yunohost/backup.py
+++ b/src/yunohost/backup.py
@@ -29,7 +29,7 @@ import json
 import yaml
 import glob
 
-from moulinette.core.helpers import YunoHostError, YunoHostLDAP, validate, colorize, win_msg
+from moulinette.helpers import YunoHostError, YunoHostLDAP, validate, colorize, win_msg
 
 def backup_init(helper=False):
     """
diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py
index 8545dacb..b10a92a7 100644
--- a/src/yunohost/domain.py
+++ b/src/yunohost/domain.py
@@ -34,7 +34,7 @@ import requests
 from urllib import urlopen
 from dyndns import dyndns_subscribe
 
-from moulinette.core.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
+from moulinette.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
 
 
 def domain_list(filter=None, limit=None, offset=None):
diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py
index 200af870..1bd6b6ba 100644
--- a/src/yunohost/dyndns.py
+++ b/src/yunohost/dyndns.py
@@ -30,7 +30,7 @@ import json
 import glob
 import base64
 
-from moulinette.core.helpers import YunoHostError, YunoHostLDAP, validate, colorize, win_msg
+from moulinette.helpers import YunoHostError, YunoHostLDAP, validate, colorize, win_msg
 
 def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None):
     """
diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py
index f55d4f59..3fb68dea 100644
--- a/src/yunohost/firewall.py
+++ b/src/yunohost/firewall.py
@@ -37,7 +37,7 @@ except ImportError:
     sys.stderr.write('apt-get install python-yaml\n')
     sys.exit(1)
 
-from moulinette.core.helpers import YunoHostError, win_msg
+from moulinette.helpers import YunoHostError, win_msg
 
 
 def firewall_allow(protocol=None, port=None, ipv6=None, upnp=False):
diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py
index 7acdc2a8..9136acfb 100644
--- a/src/yunohost/hook.py
+++ b/src/yunohost/hook.py
@@ -28,7 +28,7 @@ import sys
 import re
 import json
 
-from moulinette.core.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize
+from moulinette.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize
 
 hook_folder = '/usr/share/yunohost/hooks/'
 
diff --git a/src/yunohost/monitor.py b/src/yunohost/monitor.py
index 36b81684..cd0a22ae 100644
--- a/src/yunohost/monitor.py
+++ b/src/yunohost/monitor.py
@@ -37,7 +37,7 @@ from datetime import datetime, timedelta
 from service import (service_enable, service_disable,
     service_start, service_stop, service_status)
 
-from moulinette.core.helpers import YunoHostError, win_msg
+from moulinette.helpers import YunoHostError, win_msg
 
 glances_uri  = 'http://127.0.0.1:61209'
 stats_path   = '/var/lib/yunohost/stats'
diff --git a/src/yunohost/service.py b/src/yunohost/service.py
index 3186608d..139e1ca2 100644
--- a/src/yunohost/service.py
+++ b/src/yunohost/service.py
@@ -28,7 +28,7 @@ import glob
 import subprocess
 import os.path
 
-from moulinette.core.helpers import YunoHostError, win_msg
+from moulinette.helpers import YunoHostError, win_msg
 
 
 def service_start(names):
diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py
index cf0408bd..dd57d47f 100644
--- a/src/yunohost/tools.py
+++ b/src/yunohost/tools.py
@@ -36,7 +36,7 @@ from dyndns import dyndns_subscribe
 from backup import backup_init
 from app import app_ssowatconf
 
-from moulinette.core.helpers import YunoHostError, YunoHostLDAP, validate, colorize, get_required_args, win_msg
+from moulinette.helpers import YunoHostError, YunoHostLDAP, validate, colorize, get_required_args, win_msg
 
 
 def tools_ldapinit(password=None):
diff --git a/src/yunohost/user.py b/src/yunohost/user.py
index 86c79577..b619a227 100644
--- a/src/yunohost/user.py
+++ b/src/yunohost/user.py
@@ -33,7 +33,7 @@ import getpass
 from domain import domain_list
 from hook import hook_callback
 
-from moulinette.core.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
+from moulinette.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
 
 def user_list(fields=None, filter=None, limit=None, offset=None):
     """