diff --git a/package-lock.json b/package-lock.json
index a3e19d097..d1b9a10b4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5087,6 +5087,11 @@
                 "node": ">=0.4.0"
             }
         },
+        "node_modules/fast-decode-uri-component": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
+            "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="
+        },
         "node_modules/fast-deep-equal": {
             "version": "3.1.3",
             "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -5120,6 +5125,14 @@
             "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
             "dev": true
         },
+        "node_modules/fast-querystring": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz",
+            "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==",
+            "dependencies": {
+                "fast-decode-uri-component": "^1.0.1"
+            }
+        },
         "node_modules/fast-safe-stringify": {
             "version": "2.1.1",
             "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
@@ -5246,6 +5259,19 @@
             "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
             "dev": true
         },
+        "node_modules/find-my-way": {
+            "version": "7.6.2",
+            "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.6.2.tgz",
+            "integrity": "sha512-0OjHn1b1nCX3eVbm9ByeEHiscPYiHLfhei1wOUU9qffQkk98wE0Lo8VrVYfSGMgnSnDh86DxedduAnBf4nwUEw==",
+            "dependencies": {
+                "fast-deep-equal": "^3.1.3",
+                "fast-querystring": "^1.0.0",
+                "safe-regex2": "^2.0.0"
+            },
+            "engines": {
+                "node": ">=14"
+            }
+        },
         "node_modules/find-root": {
             "version": "1.1.0",
             "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
@@ -10082,6 +10108,14 @@
                 "node": ">=4"
             }
         },
+        "node_modules/ret": {
+            "version": "0.2.2",
+            "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz",
+            "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==",
+            "engines": {
+                "node": ">=4"
+            }
+        },
         "node_modules/retimer": {
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/retimer/-/retimer-3.0.0.tgz",
@@ -10178,6 +10212,14 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/safe-regex2": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz",
+            "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==",
+            "dependencies": {
+                "ret": "~0.2.0"
+            }
+        },
         "node_modules/safer-buffer": {
             "version": "2.1.2",
             "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -12676,6 +12718,7 @@
                 "basic-auth": "2.0.1",
                 "client-oauth2": "^4.2.5",
                 "eventsource": "^2.0.2",
+                "find-my-way": "^7.6.2",
                 "node-fetch": "^2.6.7",
                 "query-string": "^7.1.1",
                 "rxjs": "5.5.11",
diff --git a/packages/binding-http/package.json b/packages/binding-http/package.json
index b70038014..686ef5c48 100644
--- a/packages/binding-http/package.json
+++ b/packages/binding-http/package.json
@@ -60,6 +60,7 @@
         "basic-auth": "2.0.1",
         "client-oauth2": "^4.2.5",
         "eventsource": "^2.0.2",
+        "find-my-way": "^7.6.2",
         "node-fetch": "^2.6.7",
         "query-string": "^7.1.1",
         "rxjs": "5.5.11",
diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts
index 571853c9c..96009df3c 100644
--- a/packages/binding-http/src/http-server.ts
+++ b/packages/binding-http/src/http-server.ts
@@ -21,7 +21,6 @@ import * as fs from "fs";
 import * as http from "http";
 import * as https from "https";
 import bauth from "basic-auth";
-import * as url from "url";
 
 import { AddressInfo } from "net";
 
@@ -32,18 +31,22 @@ import Servient, {
     Helpers,
     ExposedThing,
     ProtocolHelpers,
-    PropertyContentMap,
-    Content,
     createLoggers,
 } from "@node-wot/core";
 import { HttpConfig, HttpForm, OAuth2ServerConfig } from "./http";
 import createValidator, { Validator } from "./oauth-token-validation";
 import { OAuth2SecurityScheme } from "@node-wot/td-tools";
 import slugify from "slugify";
-import { ThingDescription } from "wot-typescript-definitions";
-import * as acceptLanguageParser from "accept-language-parser";
 import { ActionElement, EventElement, PropertyElement } from "wot-thing-description-types";
 import { MiddlewareRequestHandler } from "./http-server-middleware";
+import Router from "find-my-way";
+import thingsRoute from "./routes/things";
+import thingDescriptionRoute from "./routes/thing-description";
+import propertyRoute from "./routes/property";
+import actionRoute from "./routes/action";
+import eventRoute from "./routes/event";
+import propertiesRoute from "./routes/properties";
+import propertyObserveRoute from "./routes/property-observe";
 
 const { debug, info, warn, error } = createLoggers("binding-http", "http-server");
 
@@ -70,6 +73,7 @@ export default class HttpServer implements ProtocolServer {
     private readonly things: Map<string, ExposedThing> = new Map<string, ExposedThing>();
     private servient: Servient = null;
     private oAuthValidator: Validator;
+    private router: Router.Instance<Router.HTTPVersion.V1>;
 
     constructor(config: HttpConfig = {}) {
         if (typeof config !== "object") {
@@ -104,6 +108,42 @@ export default class HttpServer implements ProtocolServer {
             this.middleware = config.middleware;
         }
 
+        const router = Router({
+            ignoreTrailingSlash: true,
+            defaultRoute(req, res) {
+                // url-rewrite feature in use ?
+                const pathname = req.url;
+                if (config.urlRewrite) {
+                    const entryUrl = pathname;
+                    const internalUrl = config.urlRewrite[entryUrl];
+                    if (internalUrl) {
+                        req.url = internalUrl;
+                        router.lookup(req, res, this);
+                        debug("[binding-http]", `URL "${entryUrl}" has been rewritten to "${pathname}"`);
+                        return;
+                    }
+                }
+
+                // No url-rewrite mapping found -> resource not found
+                res.writeHead(404);
+                res.end("Not Found");
+            },
+        });
+
+        this.router = router;
+
+        this.router.get("/", thingsRoute);
+        this.router.get("/:thing", thingDescriptionRoute);
+        this.router.on(["GET", "HEAD", "OPTIONS"], "/:thing/" + this.PROPERTY_DIR, propertiesRoute);
+        this.router.on(["GET", "PUT", "HEAD", "OPTIONS"], "/:thing/" + this.PROPERTY_DIR + "/:property", propertyRoute);
+        this.router.on(
+            ["GET", "HEAD", "OPTIONS"],
+            "/:thing/" + this.PROPERTY_DIR + "/:property/" + this.OBSERVABLE_DIR,
+            propertyObserveRoute
+        );
+        this.router.on(["POST", "OPTIONS"], "/:thing/" + this.ACTION_DIR + "/:action", actionRoute);
+        this.router.on(["GET", "HEAD", "OPTIONS"], "/:thing/" + this.EVENT_DIR + "/:event", eventRoute);
+
         // TLS
         if (config.serverKey && config.serverCert) {
             const options: https.ServerOptions = {};
@@ -209,6 +249,10 @@ export default class HttpServer implements ProtocolServer {
         return this.server;
     }
 
+    public getThings(): Map<string, ExposedThing> {
+        return this.things;
+    }
+
     /** returns server port number and indicates that server is running when larger than -1  */
     public getPort(): number {
         if (this.server.address() && typeof this.server.address() === "object") {
@@ -455,7 +499,7 @@ export default class HttpServer implements ProtocolServer {
         }
     }
 
-    private async checkCredentials(thing: ExposedThing, req: http.IncomingMessage): Promise<boolean> {
+    public async checkCredentials(thing: ExposedThing, req: http.IncomingMessage): Promise<boolean> {
         debug(`HttpServer on port ${this.getPort()} checking credentials for '${thing.id}'`);
 
         const creds = this.servient.getCredentials(thing.id);
@@ -538,91 +582,8 @@ export default class HttpServer implements ProtocolServer {
         }
     }
 
-    /**
-     * Look for language negotiation through the Accept-Language header field of HTTP (e.g., "de", "de-CH", "en-US,en;q=0.5")
-     * Note: "title" on thing level is mandatory term --> check whether "titles" exists for multi-languages
-     * Note: HTTP header names are case-insensitive and req.headers seems to contain them in lowercase
-     *
-     *
-     * @param td
-     * @param thing
-     * @param req
-     */
-    private negotiateLanguage(td: ThingDescription, thing: ExposedThing, req: http.IncomingMessage) {
-        if (req.headers["accept-language"] && req.headers["accept-language"] !== "*") {
-            if (thing.titles) {
-                const supportedLanguages = Object.keys(thing.titles); // e.g., ['fr', 'en']
-
-                // the loose option allows partial matching on supported languages (e.g., returns "de" for "de-CH")
-                const prefLang = acceptLanguageParser.pick(supportedLanguages, req.headers["accept-language"], {
-                    loose: true,
-                });
-
-                if (prefLang) {
-                    // if a preferred language can be found use it
-                    debug(
-                        `TD language negotiation through the Accept-Language header field of HTTP leads to "${prefLang}"`
-                    );
-                    this.resetMultiLangThing(td, prefLang);
-                }
-            }
-        }
-    }
-
-    private async handleTdRequest(thing: ExposedThing, req: http.IncomingMessage, res: http.ServerResponse) {
-        const td = thing.getThingDescription();
-        const contentSerdes = ContentSerdes.get();
-
-        // TODO: Parameters need to be considered here as well
-        const acceptValues = req.headers.accept?.split(",").map((acceptValue) => acceptValue.split(";")[0]) ?? [
-            ContentSerdes.TD,
-        ];
-
-        // TODO: Better handling of wildcard values
-        const filteredAcceptValues = acceptValues
-            .map((acceptValue) => {
-                if (acceptValue === "*/*") {
-                    return ContentSerdes.TD;
-                }
-
-                return acceptValue;
-            })
-            .filter((acceptValue) => contentSerdes.isSupported(acceptValue))
-            .sort((a, b) => {
-                // weight function last places weight more than first: application/td+json > application/json > text/html
-                const aWeight = ["text/html", "application/json", "application/td+json"].findIndex(
-                    (value) => value === a
-                );
-                const bWeight = ["text/html", "application/json", "application/td+json"].findIndex(
-                    (value) => value === b
-                );
-
-                return bWeight - aWeight;
-            });
-
-        if (filteredAcceptValues.length > 0) {
-            const contentType = filteredAcceptValues[0];
-
-            const content = contentSerdes.valueToContent(thing.getThingDescription(), undefined, contentType);
-            const payload = await content.toBuffer();
-
-            this.negotiateLanguage(td, thing, req);
-            res.setHeader("Content-Type", contentType);
-            res.writeHead(200);
-            debug(`Sending HTTP response for TD with Content-Type ${contentType}.`);
-            res.end(payload);
-            return;
-        }
-
-        debug(`Request contained an accept header with the values ${acceptValues}, none of which are supported.`);
-
-        res.writeHead(406);
-        res.end(`Accept header contained no Content-Types supported by this resource. (Was ${acceptValues})`);
-    }
-
     private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
-        // eslint-disable-next-line node/no-deprecated-api
-        const requestUri = url.parse(req.url);
+        const requestUri = new URL(req.url, `${this.scheme}://${req.headers.host}`);
 
         debug(
             `HttpServer on port ${this.getPort()} received '${req.method} ${
@@ -637,39 +598,6 @@ export default class HttpServer implements ProtocolServer {
             );
         });
 
-        // Handle requests where the path is correct and the HTTP method is not allowed.
-        function respondUnallowedMethod(
-            res: http.ServerResponse,
-            allowed: string,
-            corsPreflightWithCredentials = false
-        ): void {
-            // Always allow OPTIONS to handle CORS pre-flight requests
-            if (!allowed.includes("OPTIONS")) {
-                allowed += ", OPTIONS";
-            }
-            if (req.method === "OPTIONS" && req.headers.origin && req.headers["access-control-request-method"]) {
-                debug(
-                    `HttpServer received an CORS preflight request from ${Helpers.toUriLiteral(
-                        req.socket.remoteAddress
-                    )}:${req.socket.remotePort}`
-                );
-                if (corsPreflightWithCredentials) {
-                    res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
-                    res.setHeader("Access-Control-Allow-Credentials", "true");
-                } else {
-                    res.setHeader("Access-Control-Allow-Origin", "*");
-                }
-                res.setHeader("Access-Control-Allow-Methods", allowed);
-                res.setHeader("Access-Control-Allow-Headers", "content-type, authorization, *");
-                res.writeHead(200);
-                res.end();
-            } else {
-                res.setHeader("Allow", allowed);
-                res.writeHead(405);
-                res.end("Method Not Allowed");
-            }
-        }
-
         // Set CORS headers
         if (this.httpSecurityScheme !== "NoSec" && req.headers.origin) {
             res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
@@ -700,435 +628,6 @@ export default class HttpServer implements ProtocolServer {
             }
         }
 
-        // url-rewrite feature in use ?
-        let pathname = requestUri.pathname;
-        if (this.urlRewrite) {
-            const entryUrl = pathname;
-            const internalUrl = this.urlRewrite[entryUrl];
-            if (internalUrl) {
-                pathname = internalUrl;
-                debug("[binding-http]", `URL "${entryUrl}" has been rewritten to "${pathname}"`);
-            }
-        }
-
-        // route request
-        let segments: string[];
-        try {
-            segments = decodeURI(pathname).split("/");
-        } catch (ex) {
-            // catch URIError, see https://github.com/eclipse-thingweb/node-wot/issues/389
-            warn(`HttpServer on port ${this.getPort()} cannot decode URI for '${requestUri.pathname}'`);
-            res.writeHead(400);
-            res.end("decodeURI error for " + requestUri.pathname);
-            return;
-        }
-
-        if (segments[1] === "") {
-            // no path -> list all Things
-            if (req.method === "GET") {
-                res.setHeader("Content-Type", ContentSerdes.DEFAULT);
-                res.writeHead(200);
-                const list = [];
-                for (const address of Helpers.getAddresses()) {
-                    // FIXME are Iterables really such a non-feature that I need array?
-                    for (const name of Array.from(this.things.keys())) {
-                        // FIXME the undefined check should NOT be necessary (however there seems to be null in it)
-                        if (name) {
-                            list.push(
-                                this.scheme +
-                                    "://" +
-                                    Helpers.toUriLiteral(address) +
-                                    ":" +
-                                    this.getPort() +
-                                    "/" +
-                                    encodeURIComponent(name)
-                            );
-                        }
-                    }
-                }
-                res.end(JSON.stringify(list));
-            } else {
-                respondUnallowedMethod(res, "GET");
-            }
-            // resource found and response sent
-            return;
-        } else {
-            // path -> select Thing
-            const thing: ExposedThing = this.things.get(segments[1]);
-            if (thing) {
-                if (segments.length === 2 || segments[2] === "") {
-                    // Thing root -> send TD
-                    if (req.method === "GET") {
-                        this.handleTdRequest(thing, req, res);
-                    } else {
-                        respondUnallowedMethod(res, "GET");
-                    }
-                    // resource found and response sent
-                    return;
-                } else {
-                    let corsPreflightWithCredentials = false;
-                    // Thing Interaction - Access Control
-                    if (this.httpSecurityScheme !== "NoSec" && !(await this.checkCredentials(thing, req))) {
-                        if (req.method === "OPTIONS" && req.headers.origin) {
-                            corsPreflightWithCredentials = true;
-                        } else {
-                            res.setHeader("WWW-Authenticate", `${this.httpSecurityScheme} realm="${thing.id}"`);
-                            res.writeHead(401);
-                            res.end();
-                            return;
-                        }
-                    }
-
-                    if (segments[2] === this.PROPERTY_DIR) {
-                        if (segments.length === 3) {
-                            // all properties
-                            if (req.method === "GET") {
-                                try {
-                                    const propMap: PropertyContentMap = await thing.handleReadAllProperties({
-                                        formIndex: 0,
-                                    });
-                                    res.setHeader("Content-Type", ContentSerdes.DEFAULT); // contentType handling?
-                                    res.writeHead(200);
-                                    const recordResponse: Record<string, unknown> = {};
-                                    for (const key of propMap.keys()) {
-                                        const content: Content = propMap.get(key);
-                                        const value = ContentSerdes.get().contentToValue(
-                                            { type: ContentSerdes.DEFAULT, body: await content.toBuffer() },
-                                            {}
-                                        );
-                                        recordResponse[key] = value;
-                                    }
-                                    res.end(JSON.stringify(recordResponse));
-                                } catch (err) {
-                                    error(
-                                        `HttpServer on port ${this.getPort()} got internal error on invoke '${
-                                            requestUri.pathname
-                                        }': ${err.message}`
-                                    );
-                                    res.writeHead(500);
-                                    res.end(err.message);
-                                }
-                            } else if (req.method === "HEAD") {
-                                res.writeHead(202);
-                                res.end();
-                            } else {
-                                // may have been OPTIONS that failed the credentials check
-                                // as a result, we pass corsPreflightWithCredentials
-                                respondUnallowedMethod(res, "GET", corsPreflightWithCredentials);
-                            }
-                            // resource found and response sent
-                            return;
-                        } else {
-                            // sub-path -> select Property
-                            const property = thing.properties[segments[3]];
-                            if (property) {
-                                const options: WoT.InteractionOptions & { formIndex: number } = {
-                                    formIndex: ProtocolHelpers.findRequestMatchingFormIndex(
-                                        property.forms,
-                                        this.scheme,
-                                        req.url,
-                                        contentType
-                                    ),
-                                };
-                                const uriVariables = Helpers.parseUrlParameters(
-                                    req.url,
-                                    thing.uriVariables,
-                                    property.uriVariables
-                                );
-                                if (!this.isEmpty(uriVariables)) {
-                                    options.uriVariables = uriVariables;
-                                }
-
-                                if (req.method === "GET") {
-                                    // check if this an observable request (longpoll)
-                                    if (segments[4] === this.OBSERVABLE_DIR) {
-                                        const listener = async (value: Content) => {
-                                            try {
-                                                // send property data
-                                                value.body.pipe(res);
-                                            } catch (err) {
-                                                if (err?.code === "ERR_HTTP_HEADERS_SENT") {
-                                                    thing.handleUnobserveProperty(segments[3], listener, options);
-                                                    return;
-                                                }
-                                                warn(
-                                                    `HttpServer on port ${this.getPort()} cannot process data for Property '${
-                                                        segments[3]
-                                                    }: ${err.message}'`
-                                                );
-                                                res.writeHead(500);
-                                                res.end("Invalid Property Data");
-                                            }
-                                        };
-
-                                        await thing.handleObserveProperty(segments[3], listener, options);
-
-                                        res.on("finish", () => {
-                                            debug(`HttpServer on port ${this.getPort()} closed connection`);
-                                            thing.handleUnobserveProperty(segments[3], listener, options);
-                                        });
-                                        res.setTimeout(60 * 60 * 1000, () =>
-                                            thing.handleUnobserveProperty(segments[3], listener, options)
-                                        );
-                                        return;
-                                    } else {
-                                        try {
-                                            const content = await thing.handleReadProperty(segments[3], options);
-                                            res.setHeader("Content-Type", content.type);
-                                            res.writeHead(200);
-                                            content.body.pipe(res);
-                                        } catch (err) {
-                                            error(
-                                                `HttpServer on port ${this.getPort()} got internal error on read '${
-                                                    requestUri.pathname
-                                                }': ${err.message}`
-                                            );
-                                            res.writeHead(500);
-                                            res.end(err.message);
-                                        }
-                                        return;
-                                    }
-                                } else if (req.method === "PUT") {
-                                    if (!property.readOnly) {
-                                        try {
-                                            await thing.handleWriteProperty(
-                                                segments[3],
-                                                new Content(contentType, req),
-                                                options
-                                            );
-                                            res.writeHead(204);
-                                            res.end("Changed");
-                                        } catch (err) {
-                                            error(
-                                                `HttpServer on port ${this.getPort()} got internal error on invoke '${
-                                                    requestUri.pathname
-                                                }': ${err.message}`
-                                            );
-                                            res.writeHead(500);
-                                            res.end(err.message);
-                                        }
-                                    } else {
-                                        respondUnallowedMethod(res, "GET, PUT");
-                                    }
-                                    // resource found and response sent
-                                    return;
-                                } else if (req.method === "HEAD") {
-                                    // HEAD support for long polling subscription
-                                    res.writeHead(202);
-                                    res.end();
-                                    return;
-                                } else {
-                                    // may have been OPTIONS that failed the credentials check
-                                    // as a result, we pass corsPreflightWithCredentials
-                                    respondUnallowedMethod(res, "GET, PUT", corsPreflightWithCredentials);
-                                    return;
-                                } // Property exists?
-                            }
-                        }
-                    } else if (segments[2] === this.ACTION_DIR) {
-                        // sub-path -> select Action
-                        const action = thing.actions[segments[3]];
-                        if (action) {
-                            if (req.method === "POST") {
-                                const options: WoT.InteractionOptions & { formIndex: number } = {
-                                    formIndex: ProtocolHelpers.findRequestMatchingFormIndex(
-                                        action.forms,
-                                        this.scheme,
-                                        req.url,
-                                        contentType
-                                    ),
-                                };
-                                const uriVariables = Helpers.parseUrlParameters(
-                                    req.url,
-                                    thing.uriVariables,
-                                    action.uriVariables
-                                );
-                                if (!this.isEmpty(uriVariables)) {
-                                    options.uriVariables = uriVariables;
-                                }
-                                try {
-                                    const output = await thing.handleInvokeAction(
-                                        segments[3],
-                                        new Content(contentType, req),
-                                        options
-                                    );
-                                    if (output) {
-                                        res.setHeader("Content-Type", output.type);
-                                        res.writeHead(200);
-                                        output.body.pipe(res);
-                                    } else {
-                                        res.writeHead(200);
-                                        res.end();
-                                    }
-                                } catch (err) {
-                                    error(
-                                        `HttpServer on port ${this.getPort()} got internal error on invoke '${
-                                            requestUri.pathname
-                                        }': ${err.message}`
-                                    );
-                                    res.writeHead(500);
-                                    res.end(err.message);
-                                }
-                            } else {
-                                // may have been OPTIONS that failed the credentials check
-                                // as a result, we pass corsPreflightWithCredentials
-                                respondUnallowedMethod(res, "POST", corsPreflightWithCredentials);
-                            }
-                            // resource found and response sent
-                            return;
-                        } // Action exists?
-                    } else if (segments[2] === this.EVENT_DIR) {
-                        // sub-path -> select Event
-                        const event = thing.events[segments[3]];
-                        if (event) {
-                            if (req.method === "GET") {
-                                const options: WoT.InteractionOptions & { formIndex: number } = {
-                                    formIndex: ProtocolHelpers.findRequestMatchingFormIndex(
-                                        event.forms,
-                                        this.scheme,
-                                        req.url,
-                                        contentType
-                                    ),
-                                };
-                                const uriVariables = Helpers.parseUrlParameters(
-                                    req.url,
-                                    thing.uriVariables,
-                                    event.uriVariables
-                                );
-                                if (!this.isEmpty(uriVariables)) {
-                                    options.uriVariables = uriVariables;
-                                }
-
-                                const listener = async (value: Content) => {
-                                    try {
-                                        // send event data
-                                        if (!res.headersSent) {
-                                            // We are polite and use the same request as long as the client
-                                            // does not close the connection (or we hit the timeout; see below).
-                                            // Therefore we are sending the headers
-                                            // only if we didn't have sent them before.
-                                            res.setHeader("Content-Type", value.type);
-                                            res.writeHead(200);
-                                        }
-                                        value.body.pipe(res);
-                                    } catch (err) {
-                                        if (err?.code === "ERR_HTTP_HEADERS_SENT") {
-                                            thing.handleUnsubscribeEvent(segments[3], listener, options);
-                                            return;
-                                        }
-                                        warn(
-                                            `HttpServer on port ${this.getPort()} cannot process data for Event '${
-                                                segments[3]
-                                            }: ${err.message}'`
-                                        );
-                                        res.writeHead(500);
-                                        res.end("Invalid Event Data");
-                                    }
-                                };
-
-                                await thing.handleSubscribeEvent(segments[3], listener, options);
-                                res.on("close", () => {
-                                    debug(`HttpServer on port ${this.getPort()} closed Event connection`);
-                                    thing.handleUnsubscribeEvent(segments[3], listener, options);
-                                });
-                                res.setTimeout(60 * 60 * 1000, () =>
-                                    thing.handleUnsubscribeEvent(segments[3], listener, options)
-                                );
-                            } else if (req.method === "HEAD") {
-                                // HEAD support for long polling subscription
-                                res.writeHead(202);
-                                res.end();
-                            } else {
-                                // may have been OPTIONS that failed the credentials check
-                                // as a result, we pass corsPreflightWithCredentials
-                                respondUnallowedMethod(res, "GET", corsPreflightWithCredentials);
-                            }
-                            // resource found and response sent
-                            return;
-                        } // Event exists?
-                    }
-                } // Interaction?
-            } // Thing exists?
-        }
-
-        // resource not found
-        res.writeHead(404);
-        res.end("Not Found");
-    }
-
-    private isEmpty(obj: Record<string, unknown>): boolean {
-        for (const key in obj) {
-            if (Object.prototype.hasOwnProperty.call(obj, key)) return false;
-        }
-        return true;
-    }
-
-    private resetMultiLangThing(thing: ThingDescription, prefLang: string) {
-        // TODO can we reset "title" to another name given that title is used in URI creation?
-
-        // set @language in @context
-        TD.setContextLanguage(thing, prefLang, true);
-
-        // use new language title
-        if (thing.titles) {
-            for (const titleLang in thing.titles) {
-                if (titleLang.startsWith(prefLang)) {
-                    thing.title = thing.titles[titleLang];
-                }
-            }
-        }
-
-        // use new language description
-        if (thing.descriptions) {
-            for (const titleLang in thing.descriptions) {
-                if (titleLang.startsWith(prefLang)) {
-                    thing.description = thing.descriptions[titleLang];
-                }
-            }
-        }
-
-        // remove any titles or descriptions and update title / description accordingly
-        delete thing.titles;
-        delete thing.descriptions;
-
-        // reset multi-language terms for interactions
-        this.resetMultiLangInteraction(thing.properties, prefLang);
-        this.resetMultiLangInteraction(thing.actions, prefLang);
-        this.resetMultiLangInteraction(thing.events, prefLang);
-    }
-
-    private resetMultiLangInteraction(
-        interactions: ThingDescription["properties"] | ThingDescription["actions"] | ThingDescription["events"],
-        prefLang: string
-    ) {
-        if (interactions) {
-            for (const interName in interactions) {
-                // unset any current title and/or description
-                delete interactions[interName].title;
-                delete interactions[interName].description;
-
-                // use new language title
-                if (interactions[interName].titles) {
-                    for (const titleLang in interactions[interName].titles) {
-                        if (titleLang.startsWith(prefLang)) {
-                            interactions[interName].title = interactions[interName].titles[titleLang];
-                        }
-                    }
-                }
-
-                // use new language description
-                if (interactions[interName].descriptions) {
-                    for (const descLang in interactions[interName].descriptions) {
-                        if (descLang.startsWith(prefLang)) {
-                            interactions[interName].description = interactions[interName].descriptions[descLang];
-                        }
-                    }
-                }
-
-                // unset any multilanguage titles and/or descriptions
-                delete interactions[interName].titles;
-                delete interactions[interName].descriptions;
-            }
-        }
+        this.router.lookup(req, res, this);
     }
 }
diff --git a/packages/binding-http/src/routes/action.ts b/packages/binding-http/src/routes/action.ts
new file mode 100644
index 000000000..4cad36d9b
--- /dev/null
+++ b/packages/binding-http/src/routes/action.ts
@@ -0,0 +1,99 @@
+/********************************************************************************
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
+ * Document License (2015-05-13) which is available at
+ * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
+ ********************************************************************************/
+import { IncomingMessage, ServerResponse } from "http";
+import { Content, Helpers, ProtocolHelpers, createLoggers } from "@node-wot/core";
+import { isEmpty, respondUnallowedMethod, validOrDefaultRequestContentType } from "./common";
+import HttpServer from "../http-server";
+
+const { error, warn } = createLoggers("binding-http", "routes", "action");
+
+export default async function actionRoute(
+    this: HttpServer,
+    req: IncomingMessage,
+    res: ServerResponse,
+    _params: { thing: string; action: string }
+): Promise<void> {
+    const thing = this.getThings().get(_params.thing);
+
+    if (!thing) {
+        res.writeHead(404);
+        res.end();
+        return;
+    }
+
+    const contentTypeHeader: string | string[] = req.headers["content-type"];
+    let contentType: string = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader;
+    try {
+        contentType = validOrDefaultRequestContentType(req, res, contentType);
+    } catch (error) {
+        warn(
+            `HttpServer received unsupported Content-Type from ${Helpers.toUriLiteral(req.socket.remoteAddress)}:${
+                req.socket.remotePort
+            }`
+        );
+        res.writeHead(415);
+        res.end("Unsupported Media Type");
+        return;
+    }
+
+    const action = thing.actions[_params.action];
+
+    if (!action) {
+        res.writeHead(404);
+        res.end();
+        return;
+    }
+    // TODO: refactor this part to move into a common place
+    let corsPreflightWithCredentials = false;
+    if (this.getHttpSecurityScheme() !== "NoSec" && !(await this.checkCredentials(thing, req))) {
+        if (req.method === "OPTIONS" && req.headers.origin) {
+            corsPreflightWithCredentials = true;
+        } else {
+            res.setHeader("WWW-Authenticate", `${this.getHttpSecurityScheme()} realm="${thing.id}"`);
+            res.writeHead(401);
+            res.end();
+            return;
+        }
+    }
+
+    if (req.method === "POST") {
+        const options: WoT.InteractionOptions & { formIndex: number } = {
+            formIndex: ProtocolHelpers.findRequestMatchingFormIndex(action.forms, this.scheme, req.url, contentType),
+        };
+        const uriVariables = Helpers.parseUrlParameters(req.url, thing.uriVariables, action.uriVariables);
+        if (!isEmpty(uriVariables)) {
+            options.uriVariables = uriVariables;
+        }
+        try {
+            const output = await thing.handleInvokeAction(_params.action, new Content(contentType, req), options);
+            if (output) {
+                res.setHeader("Content-Type", output.type);
+                res.writeHead(200);
+                output.body.pipe(res);
+            } else {
+                res.writeHead(200);
+                res.end();
+            }
+        } catch (err) {
+            error(`HttpServer on port ${this.getPort()} got internal error on invoke '${req.url}': ${err.message}`);
+            res.writeHead(500);
+            res.end(err.message);
+        }
+    } else {
+        // may have been OPTIONS that failed the credentials check
+        // as a result, we pass corsPreflightWithCredentials
+        respondUnallowedMethod(req, res, "POST", corsPreflightWithCredentials);
+    }
+}
diff --git a/packages/binding-http/src/routes/common.ts b/packages/binding-http/src/routes/common.ts
new file mode 100644
index 000000000..d7012347c
--- /dev/null
+++ b/packages/binding-http/src/routes/common.ts
@@ -0,0 +1,81 @@
+/********************************************************************************
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
+ * Document License (2015-05-13) which is available at
+ * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
+ ********************************************************************************/
+import { ContentSerdes, Helpers, createLoggers } from "@node-wot/core";
+import { IncomingMessage, ServerResponse } from "http";
+
+const { debug, warn } = createLoggers("binding-http", "routes", "common");
+
+export function respondUnallowedMethod(
+    req: IncomingMessage,
+    res: ServerResponse,
+    allowed: string,
+    corsPreflightWithCredentials = false
+): void {
+    // Always allow OPTIONS to handle CORS pre-flight requests
+    if (!allowed.includes("OPTIONS")) {
+        allowed += ", OPTIONS";
+    }
+    if (req.method === "OPTIONS" && req.headers.origin && req.headers["access-control-request-method"]) {
+        debug(
+            `HttpServer received an CORS preflight request from ${Helpers.toUriLiteral(req.socket.remoteAddress)}:${
+                req.socket.remotePort
+            }`
+        );
+        if (corsPreflightWithCredentials) {
+            res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
+            res.setHeader("Access-Control-Allow-Credentials", "true");
+        } else {
+            res.setHeader("Access-Control-Allow-Origin", "*");
+        }
+        res.setHeader("Access-Control-Allow-Methods", allowed);
+        res.setHeader("Access-Control-Allow-Headers", "content-type, authorization, *");
+        res.writeHead(200);
+        res.end();
+    } else {
+        res.setHeader("Allow", allowed);
+        res.writeHead(405);
+        res.end("Method Not Allowed");
+    }
+}
+
+export function validOrDefaultRequestContentType(
+    req: IncomingMessage,
+    res: ServerResponse,
+    contentType: string
+): string {
+    if (req.method === "PUT" || req.method === "POST") {
+        if (!contentType) {
+            // FIXME should be rejected with 400 Bad Request, as guessing is not good in M2M -> debug/testing flag to allow
+            // FIXME would need to check if payload is present
+            warn(
+                `HttpServer received no Content-Type from ${Helpers.toUriLiteral(req.socket.remoteAddress)}:${
+                    req.socket.remotePort
+                }`
+            );
+            return ContentSerdes.DEFAULT;
+        } else if (ContentSerdes.get().getSupportedMediaTypes().indexOf(ContentSerdes.getMediaType(contentType)) < 0) {
+            throw new Error("Unsupported Media Type");
+        }
+        return contentType;
+    }
+    return contentType;
+}
+
+export function isEmpty(obj: Record<string, unknown>): boolean {
+    for (const key in obj) {
+        if (Object.prototype.hasOwnProperty.call(obj, key)) return false;
+    }
+    return true;
+}
diff --git a/packages/binding-http/src/routes/event.ts b/packages/binding-http/src/routes/event.ts
new file mode 100644
index 000000000..4f1c43d55
--- /dev/null
+++ b/packages/binding-http/src/routes/event.ts
@@ -0,0 +1,108 @@
+/********************************************************************************
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
+ * Document License (2015-05-13) which is available at
+ * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
+ ********************************************************************************/
+import { IncomingMessage, ServerResponse } from "http";
+import { Content, Helpers, ProtocolHelpers, createLoggers } from "@node-wot/core";
+import { isEmpty, respondUnallowedMethod } from "./common";
+import HttpServer from "../http-server";
+
+const { warn, debug } = createLoggers("binding-http", "routes", "event");
+export default async function eventRoute(
+    this: HttpServer,
+    req: IncomingMessage,
+    res: ServerResponse,
+    _params: { thing: string; event: string }
+): Promise<void> {
+    const thing = this.getThings().get(_params.thing);
+
+    if (!thing) {
+        res.writeHead(404);
+        res.end();
+        return;
+    }
+
+    const contentTypeHeader: string | string[] = req.headers["content-type"];
+    const contentType: string = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader;
+
+    const event = thing.events[_params.event];
+    if (!event) {
+        res.writeHead(404);
+        res.end();
+        return;
+    }
+    // TODO: refactor this part to move into a common place
+    let corsPreflightWithCredentials = false;
+    if (this.getHttpSecurityScheme() !== "NoSec" && !(await this.checkCredentials(thing, req))) {
+        if (req.method === "OPTIONS" && req.headers.origin) {
+            corsPreflightWithCredentials = true;
+        } else {
+            res.setHeader("WWW-Authenticate", `${this.getHttpSecurityScheme()} realm="${thing.id}"`);
+            res.writeHead(401);
+            res.end();
+            return;
+        }
+    }
+
+    if (req.method === "GET") {
+        const options: WoT.InteractionOptions & { formIndex: number } = {
+            formIndex: ProtocolHelpers.findRequestMatchingFormIndex(event.forms, this.scheme, req.url, contentType),
+        };
+        const uriVariables = Helpers.parseUrlParameters(req.url, thing.uriVariables, event.uriVariables);
+        if (!isEmpty(uriVariables)) {
+            options.uriVariables = uriVariables;
+        }
+        const listener = async (value: Content) => {
+            try {
+                // send event data
+                if (!res.headersSent) {
+                    // We are polite and use the same request as long as the client
+                    // does not close the connection (or we hit the timeout; see below).
+                    // Therefore we are sending the headers
+                    // only if we didn't have sent them before.
+                    res.setHeader("Content-Type", value.type);
+                    res.writeHead(200);
+                }
+                value.body.pipe(res);
+            } catch (err) {
+                if (err?.code === "ERR_HTTP_HEADERS_SENT") {
+                    thing.handleUnsubscribeEvent(_params.event, listener, options);
+                    return;
+                }
+                warn(
+                    `HttpServer on port ${this.getPort()} cannot process data for Event '${_params.event}: ${
+                        err.message
+                    }'`
+                );
+                res.writeHead(500);
+                res.end("Invalid Event Data");
+            }
+        };
+
+        await thing.handleSubscribeEvent(_params.event, listener, options);
+        res.on("close", () => {
+            debug(`HttpServer on port ${this.getPort()} closed Event connection`);
+            thing.handleUnsubscribeEvent(_params.event, listener, options);
+        });
+        res.setTimeout(60 * 60 * 1000, () => thing.handleUnsubscribeEvent(_params.event, listener, options));
+    } else if (req.method === "HEAD") {
+        // HEAD support for long polling subscription
+        res.writeHead(202);
+        res.end();
+    } else {
+        // may have been OPTIONS that failed the credentials check
+        // as a result, we pass corsPreflightWithCredentials
+        respondUnallowedMethod(req, res, "GET", corsPreflightWithCredentials);
+    }
+    // resource found and response sent
+}
diff --git a/packages/binding-http/src/routes/properties.ts b/packages/binding-http/src/routes/properties.ts
new file mode 100644
index 000000000..dd9e5f6c0
--- /dev/null
+++ b/packages/binding-http/src/routes/properties.ts
@@ -0,0 +1,79 @@
+/********************************************************************************
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
+ * Document License (2015-05-13) which is available at
+ * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
+ ********************************************************************************/
+import { IncomingMessage, ServerResponse } from "http";
+import { Content, ContentSerdes, PropertyContentMap, createLoggers } from "@node-wot/core";
+import { respondUnallowedMethod } from "./common";
+import HttpServer from "../http-server";
+
+const { error } = createLoggers("binding-http", "routes", "properties");
+export default async function propertiesRoute(
+    this: HttpServer,
+    req: IncomingMessage,
+    res: ServerResponse,
+    _params: { thing: string }
+): Promise<void> {
+    const thing = this.getThings().get(_params.thing);
+
+    if (!thing) {
+        res.writeHead(404);
+        res.end();
+        return;
+    }
+
+    // TODO: refactor this part to move into a common place
+    let corsPreflightWithCredentials = false;
+    if (this.getHttpSecurityScheme() !== "NoSec" && !(await this.checkCredentials(thing, req))) {
+        if (req.method === "OPTIONS" && req.headers.origin) {
+            corsPreflightWithCredentials = true;
+        } else {
+            res.setHeader("WWW-Authenticate", `${this.getHttpSecurityScheme()} realm="${thing.id}"`);
+            res.writeHead(401);
+            res.end();
+            return;
+        }
+    }
+
+    // all properties
+    if (req.method === "GET") {
+        try {
+            const propMap: PropertyContentMap = await thing.handleReadAllProperties({
+                formIndex: 0,
+            });
+            res.setHeader("Content-Type", ContentSerdes.DEFAULT); // contentType handling?
+            res.writeHead(200);
+            const recordResponse: Record<string, unknown> = {};
+            for (const key of propMap.keys()) {
+                const content: Content = propMap.get(key);
+                const value = ContentSerdes.get().contentToValue(
+                    { type: ContentSerdes.DEFAULT, body: await content.toBuffer() },
+                    {}
+                );
+                recordResponse[key] = value;
+            }
+            res.end(JSON.stringify(recordResponse));
+        } catch (err) {
+            error(`HttpServer on port ${this.getPort()} got internal error on invoke '${req.url}': ${err.message}`);
+            res.writeHead(500);
+            res.end(err.message);
+        }
+    } else if (req.method === "HEAD") {
+        res.writeHead(202);
+        res.end();
+    } else {
+        // may have been OPTIONS that failed the credentials check
+        // as a result, we pass corsPreflightWithCredentials
+        respondUnallowedMethod(req, res, "GET", corsPreflightWithCredentials);
+    }
+}
diff --git a/packages/binding-http/src/routes/property-observe.ts b/packages/binding-http/src/routes/property-observe.ts
new file mode 100644
index 000000000..f31e4eb62
--- /dev/null
+++ b/packages/binding-http/src/routes/property-observe.ts
@@ -0,0 +1,98 @@
+/********************************************************************************
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
+ * Document License (2015-05-13) which is available at
+ * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
+ ********************************************************************************/
+import { IncomingMessage, ServerResponse } from "http";
+import { Content, Helpers, ProtocolHelpers, createLoggers } from "@node-wot/core";
+import { isEmpty, respondUnallowedMethod } from "./common";
+import HttpServer from "../http-server";
+
+const { debug, warn } = createLoggers("binding-http", "routes", "property", "observe");
+export default async function propertyObserveRoute(
+    this: HttpServer,
+    req: IncomingMessage,
+    res: ServerResponse,
+    _params: { thing: string; property: string }
+): Promise<void> {
+    const thing = this.getThings().get(_params.thing);
+
+    if (!thing) {
+        res.writeHead(404);
+        res.end();
+        return;
+    }
+
+    const contentTypeHeader: string | string[] = req.headers["content-type"];
+    const contentType: string = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader;
+    const property = thing.properties[_params.property];
+
+    if (!property) {
+        res.writeHead(404);
+        res.end();
+        return;
+    }
+
+    const options: WoT.InteractionOptions & { formIndex: number } = {
+        formIndex: ProtocolHelpers.findRequestMatchingFormIndex(property.forms, this.scheme, req.url, contentType),
+    };
+    const uriVariables = Helpers.parseUrlParameters(req.url, thing.uriVariables, property.uriVariables);
+    if (!isEmpty(uriVariables)) {
+        options.uriVariables = uriVariables;
+    }
+
+    // TODO: refactor this part to move into a common place
+    let corsPreflightWithCredentials = false;
+    if (this.getHttpSecurityScheme() !== "NoSec" && !(await this.checkCredentials(thing, req))) {
+        if (req.method === "OPTIONS" && req.headers.origin) {
+            corsPreflightWithCredentials = true;
+        } else {
+            res.setHeader("WWW-Authenticate", `${this.getHttpSecurityScheme()} realm="${thing.id}"`);
+            res.writeHead(401);
+            res.end();
+            return;
+        }
+    }
+
+    if (req.method === "GET") {
+        const listener = async (value: Content) => {
+            try {
+                // send property data
+                value.body.pipe(res);
+            } catch (err) {
+                if (err?.code === "ERR_HTTP_HEADERS_SENT") {
+                    thing.handleUnobserveProperty(_params.property, listener, options);
+                    return;
+                }
+                warn(
+                    `HttpServer on port ${this.getPort()} cannot process data for Property '${_params.property}: ${
+                        err.message
+                    }'`
+                );
+                res.writeHead(500);
+                res.end("Invalid Property Data");
+            }
+        };
+        await thing.handleObserveProperty(_params.property, listener, options);
+        res.on("finish", () => {
+            debug(`HttpServer on port ${this.getPort()} closed connection`);
+            thing.handleUnobserveProperty(_params.property, listener, options);
+        });
+        res.setTimeout(60 * 60 * 1000, () => thing.handleUnobserveProperty(_params.property, listener, options));
+    } else if (req.method === "HEAD") {
+        // HEAD support for long polling subscription
+        res.writeHead(202);
+        res.end();
+    } else {
+        respondUnallowedMethod(req, res, "GET", corsPreflightWithCredentials);
+    }
+}
diff --git a/packages/binding-http/src/routes/property.ts b/packages/binding-http/src/routes/property.ts
new file mode 100644
index 000000000..71445a264
--- /dev/null
+++ b/packages/binding-http/src/routes/property.ts
@@ -0,0 +1,112 @@
+/********************************************************************************
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
+ * Document License (2015-05-13) which is available at
+ * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
+ ********************************************************************************/
+import { IncomingMessage, ServerResponse } from "http";
+import { Content, Helpers, ProtocolHelpers, createLoggers } from "@node-wot/core";
+import { isEmpty, respondUnallowedMethod, validOrDefaultRequestContentType } from "./common";
+import HttpServer from "../http-server";
+
+const { error, warn } = createLoggers("binding-http", "routes", "property");
+export default async function propertyRoute(
+    this: HttpServer,
+    req: IncomingMessage,
+    res: ServerResponse,
+    _params: { thing: string; property: string }
+): Promise<void> {
+    const thing = this.getThings().get(_params.thing);
+
+    if (!thing) {
+        res.writeHead(404);
+        res.end();
+        return;
+    }
+
+    const contentTypeHeader: string | string[] = req.headers["content-type"];
+    let contentType: string = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader;
+    try {
+        contentType = validOrDefaultRequestContentType(req, res, contentType);
+    } catch (error) {
+        warn(
+            `HttpServer received unsupported Content-Type from ${Helpers.toUriLiteral(req.socket.remoteAddress)}:${
+                req.socket.remotePort
+            }`
+        );
+        res.writeHead(415);
+        res.end("Unsupported Media Type");
+        return;
+    }
+
+    const property = thing.properties[_params.property];
+
+    if (!property) {
+        res.writeHead(404);
+        res.end();
+        return;
+    }
+
+    const options: WoT.InteractionOptions & { formIndex: number } = {
+        formIndex: ProtocolHelpers.findRequestMatchingFormIndex(property.forms, this.scheme, req.url, contentType),
+    };
+    const uriVariables = Helpers.parseUrlParameters(req.url, thing.uriVariables, property.uriVariables);
+
+    if (!isEmpty(uriVariables)) {
+        options.uriVariables = uriVariables;
+    }
+
+    // TODO: refactor this part to move into a common place
+    let corsPreflightWithCredentials = false;
+    if (this.getHttpSecurityScheme() !== "NoSec" && !(await this.checkCredentials(thing, req))) {
+        if (req.method === "OPTIONS" && req.headers.origin) {
+            corsPreflightWithCredentials = true;
+        } else {
+            res.setHeader("WWW-Authenticate", `${this.getHttpSecurityScheme()} realm="${thing.id}"`);
+            res.writeHead(401);
+            res.end();
+            return;
+        }
+    }
+
+    if (req.method === "GET") {
+        try {
+            const content = await thing.handleReadProperty(_params.property, options);
+            res.setHeader("Content-Type", content.type);
+            res.writeHead(200);
+            content.body.pipe(res);
+        } catch (err) {
+            error(`HttpServer on port ${this.getPort()} got internal error on read '${req.url}': ${err.message}`);
+            res.writeHead(500);
+            res.end(err.message);
+        }
+    } else if (req.method === "PUT") {
+        if (!property.readOnly) {
+            try {
+                await thing.handleWriteProperty(_params.property, new Content(contentType, req), options);
+
+                res.writeHead(204);
+                res.end("Changed");
+            } catch (err) {
+                error(`HttpServer on port ${this.getPort()} got internal error on invoke '${req.url}': ${err.message}`);
+                res.writeHead(500);
+                res.end(err.message);
+            }
+        } else {
+            respondUnallowedMethod(req, res, "GET, PUT");
+        }
+        // resource found and response sent
+    } else {
+        // may have been OPTIONS that failed the credentials check
+        // as a result, we pass corsPreflightWithCredentials
+        respondUnallowedMethod(req, res, "GET, PUT", corsPreflightWithCredentials);
+    } // Property exists?
+}
diff --git a/packages/binding-http/src/routes/thing-description.ts b/packages/binding-http/src/routes/thing-description.ts
new file mode 100644
index 000000000..c05e4b133
--- /dev/null
+++ b/packages/binding-http/src/routes/thing-description.ts
@@ -0,0 +1,177 @@
+/********************************************************************************
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
+ * Document License (2015-05-13) which is available at
+ * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
+ ********************************************************************************/
+
+import { ContentSerdes, createLoggers } from "@node-wot/core";
+import { IncomingMessage, ServerResponse } from "http";
+import { ExposedThing, ThingDescription } from "wot-typescript-definitions";
+import * as acceptLanguageParser from "accept-language-parser";
+import * as TD from "@node-wot/td-tools";
+import HttpServer from "../http-server";
+
+const { debug } = createLoggers("binding-http", "routes", "thing-description");
+
+function resetMultiLangInteraction(
+    interactions: ThingDescription["properties"] | ThingDescription["actions"] | ThingDescription["events"],
+    prefLang: string
+) {
+    if (interactions) {
+        for (const interName in interactions) {
+            // unset any current title and/or description
+            delete interactions[interName].title;
+            delete interactions[interName].description;
+
+            // use new language title
+            if (interactions[interName].titles) {
+                for (const titleLang in interactions[interName].titles) {
+                    if (titleLang.startsWith(prefLang)) {
+                        interactions[interName].title = interactions[interName].titles[titleLang];
+                    }
+                }
+            }
+
+            // use new language description
+            if (interactions[interName].descriptions) {
+                for (const descLang in interactions[interName].descriptions) {
+                    if (descLang.startsWith(prefLang)) {
+                        interactions[interName].description = interactions[interName].descriptions[descLang];
+                    }
+                }
+            }
+
+            // unset any multilanguage titles and/or descriptions
+            delete interactions[interName].titles;
+            delete interactions[interName].descriptions;
+        }
+    }
+}
+
+function resetMultiLangThing(thing: ThingDescription, prefLang: string) {
+    // TODO can we reset "title" to another name given that title is used in URI creation?
+
+    // set @language in @context
+    TD.setContextLanguage(thing, prefLang, true);
+
+    // use new language title
+    if (thing.titles) {
+        for (const titleLang in thing.titles) {
+            if (titleLang.startsWith(prefLang)) {
+                thing.title = thing.titles[titleLang];
+            }
+        }
+    }
+
+    // use new language description
+    if (thing.descriptions) {
+        for (const titleLang in thing.descriptions) {
+            if (titleLang.startsWith(prefLang)) {
+                thing.description = thing.descriptions[titleLang];
+            }
+        }
+    }
+
+    // remove any titles or descriptions and update title / description accordingly
+    delete thing.titles;
+    delete thing.descriptions;
+
+    // reset multi-language terms for interactions
+    resetMultiLangInteraction(thing.properties, prefLang);
+    resetMultiLangInteraction(thing.actions, prefLang);
+    resetMultiLangInteraction(thing.events, prefLang);
+}
+
+/**
+ * Look for language negotiation through the Accept-Language header field of HTTP (e.g., "de", "de-CH", "en-US,en;q=0.5")
+ * Note: "title" on thing level is mandatory term --> check whether "titles" exists for multi-languages
+ * Note: HTTP header names are case-insensitive and req.headers seems to contain them in lowercase
+ *
+ *
+ * @param td
+ * @param thing
+ * @param req
+ */
+function negotiateLanguage(td: ThingDescription, thing: ExposedThing, req: IncomingMessage) {
+    if (req.headers["accept-language"] && req.headers["accept-language"] !== "*") {
+        if (td.titles) {
+            const supportedLanguages = Object.keys(td.titles); // e.g., ['fr', 'en']
+            // the loose option allows partial matching on supported languages (e.g., returns "de" for "de-CH")
+            const prefLang = acceptLanguageParser.pick(supportedLanguages, req.headers["accept-language"], {
+                loose: true,
+            });
+            if (prefLang) {
+                // if a preferred language can be found use it
+                debug(
+                    `TD language negotiation through the Accept-Language header field of HTTP leads to "${prefLang}"`
+                );
+                // TODO: reset titles and descriptions to only contain the preferred language
+                resetMultiLangThing(td, prefLang);
+            }
+        }
+    }
+}
+
+export default async function thingDescriptionRoute(
+    this: HttpServer,
+    req: IncomingMessage,
+    res: ServerResponse,
+    _params: { thing: string }
+): Promise<void> {
+    const thing = this.getThings().get(_params.thing);
+    if (!thing) {
+        res.writeHead(404);
+        res.end();
+        return;
+    }
+
+    const td = thing.getThingDescription();
+    const contentSerdes = ContentSerdes.get();
+
+    // TODO: Parameters need to be considered here as well
+    const acceptValues = req.headers.accept?.split(",").map((acceptValue) => acceptValue.split(";")[0]) ?? [
+        ContentSerdes.TD,
+    ];
+    // TODO: Better handling of wildcard values
+    const filteredAcceptValues = acceptValues
+        .map((acceptValue) => {
+            if (acceptValue === "*/*") {
+                return ContentSerdes.TD;
+            }
+            return acceptValue;
+        })
+        .filter((acceptValue) => contentSerdes.isSupported(acceptValue))
+        .sort((a, b) => {
+            // weight function last places weight more than first: application/td+json > application/json > text/html
+            const aWeight = ["text/html", "application/json", "application/td+json"].findIndex((value) => value === a);
+            const bWeight = ["text/html", "application/json", "application/td+json"].findIndex((value) => value === b);
+            return bWeight - aWeight;
+        });
+
+    if (filteredAcceptValues.length > 0) {
+        const contentType = filteredAcceptValues[0];
+        const content = contentSerdes.valueToContent(td, undefined, contentType);
+        const payload = await content.toBuffer();
+
+        negotiateLanguage(td, thing, req);
+
+        res.setHeader("Content-Type", contentType);
+        res.writeHead(200);
+        debug(`Sending HTTP response for TD with Content-Type ${contentType}.`);
+        res.end(payload);
+        return;
+    }
+
+    debug(`Request contained an accept header with the values ${acceptValues}, none of which are supported.`);
+    res.writeHead(406);
+    res.end(`Accept header contained no Content-Types supported by this resource. (Was ${acceptValues})`);
+}
diff --git a/packages/binding-http/src/routes/things.ts b/packages/binding-http/src/routes/things.ts
new file mode 100644
index 000000000..0a5464e97
--- /dev/null
+++ b/packages/binding-http/src/routes/things.ts
@@ -0,0 +1,45 @@
+/********************************************************************************
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
+ * Document License (2015-05-13) which is available at
+ * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
+ ********************************************************************************/
+import { ContentSerdes, Helpers } from "@node-wot/core";
+import { IncomingMessage, ServerResponse } from "http";
+import HttpServer from "../http-server";
+
+export default function thingsRoute(
+    this: HttpServer,
+    req: IncomingMessage,
+    res: ServerResponse,
+    _params: unknown
+): void {
+    res.setHeader("Content-Type", ContentSerdes.DEFAULT);
+    res.writeHead(200);
+    const list = [];
+    for (const address of Helpers.getAddresses()) {
+        for (const name in this.getThings()) {
+            // FIXME the undefined check should NOT be necessary (however there seems to be null in it)
+            if (name) {
+                list.push(
+                    this.scheme +
+                        "://" +
+                        Helpers.toUriLiteral(address) +
+                        ":" +
+                        this.getPort() +
+                        "/" +
+                        encodeURIComponent(name)
+                );
+            }
+        }
+    }
+    res.end(JSON.stringify(list));
+}
diff --git a/packages/binding-http/test/http-server-oauth-tests.ts b/packages/binding-http/test/http-server-oauth-tests.ts
index ecfafdfc7..59adb710f 100644
--- a/packages/binding-http/test/http-server-oauth-tests.ts
+++ b/packages/binding-http/test/http-server-oauth-tests.ts
@@ -93,7 +93,7 @@ class OAuthServerTests {
             return true;
         };
 
-        await fetch("http://localhost:8080/testoauth/TestOAuth");
+        await fetch("http://localhost:8080/testoauth/properties/test");
 
         called.should.eql(true);
     }
@@ -107,7 +107,7 @@ class OAuthServerTests {
             return false;
         };
 
-        const response = await fetch("http://localhost:8080/testoauth/TestOAuth");
+        const response = await fetch("http://localhost:8080/testoauth/properties/test");
 
         called.should.eql(true);
 
@@ -123,7 +123,7 @@ class OAuthServerTests {
             return false;
         };
 
-        const response = await fetch("http://localhost:8080/testoauth/TestOAuth");
+        const response = await fetch("http://localhost:8080/testoauth/properties/test");
 
         called.should.eql(true);
 
diff --git a/packages/binding-http/test/http-server-test.ts b/packages/binding-http/test/http-server-test.ts
index 589a49e4c..ff69c9f66 100644
--- a/packages/binding-http/test/http-server-test.ts
+++ b/packages/binding-http/test/http-server-test.ts
@@ -779,7 +779,7 @@ class HttpServerTest {
 
         await httpServer.expose(testThing);
 
-        const uri = `http://localhost:${httpServer.getPort()}/test/`;
+        const uri = `http://localhost:${httpServer.getPort()}/test`;
 
         const testCases = [
             {
@@ -853,7 +853,7 @@ class HttpServerTest {
 
         await httpServer.expose(testThing);
 
-        const uri = `http://localhost:${httpServer.getPort()}/test/`;
+        const uri = `http://localhost:${httpServer.getPort()}/test`;
 
         const failedNegotiationResponse = await fetch(uri, {
             headers: {