Skip to main content

Building a Thing - Part 1

Introduction​

So far in this series, we've covered how Thing Descriptions are structured, what Interaction Affordances are, and how Forms bind them to real network endpoints. Now it's time to put those concepts into practice by building an actual Thing.

In this tutorial you'll implement the smart coffee machine we've been using as a running example throughout the series. By the end, you'll have a running server that exposes two readable properties over HTTP and a Thing Description that describes them.

We'll build it two ways, side by side:

  • node-wot — a reference implementation of the WoT Scripting API, provided by the Eclipse Thingweb project. It generates the Thing Description and handles all HTTP routing automatically.
  • Express.js — a popular Node.js framework. We'll build the same Thing manually alongside it, so you can see exactly what node-wot is doing under the hood.

What We're Building​

The coffee machine will expose two read-only properties in this tutorial:

AffordanceTypeDescription
coffeeBeansLeftPropertyAmount of coffee beans remaining (grams)
waterLevelPropertyCurrent water level (milliliters)

We'll add the brewCoffee action in the next tutorial.

Setting Up Your Project​

OS differences

This tutorial assumes a Unix-like shell (macOS, Linux, or Windows Git Bash) for commands like mkdir and cd. Windows PowerShell users will need to adapt these to their equivalents (for example New-Item -ItemType Directory and Set-Location).

Create a project folder and initialise it:

mkdir coffee-machine-thing
cd coffee-machine-thing
npm init -y

Now install the dependencies for whichever approach you're following:

npm install @node-wot/core @node-wot/binding-http
  • @node-wot/core — the WoT runtime engine
  • @node-wot/binding-http — the HTTP protocol binding

Create a file called thing.js in the project folder. That's where the implementation lives.

Defining the Properties​

A property affordance in the TD carries the data schema (type, minimum, maximum, readOnly) and a forms array that tells Consumers how to read it. With node-wot you define this in code and the TD is generated for you; with Express you maintain it as a JavaScript object.

In node-wot the runtime is called a Servient. We create one, attach an HTTP server, and start it. Everything else happens inside the .then() callback once the runtime is ready. We call WoT.produce() with an object that mirrors the TD structure — node-wot registers all HTTP routes and generates the full TD automatically.

const { Servient } = require("@node-wot/core");
const { HttpServer } = require("@node-wot/binding-http");

let coffeeBeansLeft = 320; // grams
let waterLevel = 1000; // milliliters

const servient = new Servient();
servient.addServer(new HttpServer({ port: 8080 }));

servient.start().then((WoT) => {
WoT.produce({
title: "Smart Coffee Machine",
description: "Remote controllable coffee machine",
id: "urn:uuid:0804d572-cce8-422a-bb7c-4412fcd56f06",
"@context": "https://www.w3.org/2022/wot/td/v1.1",
properties: {
coffeeBeansLeft: {
title: "Remaining Coffee Beans",
type: "number",
minimum: 0,
maximum: 500,
unit: "g",
readOnly: true,
observable: false,
},
waterLevel: {
title: "Water Level",
type: "number",
minimum: 0,
maximum: 1000,
unit: "ml",
readOnly: true,
observable: false,
}
}
}).then((thing) => {
// Read handlers and expose go here
});
});

Attaching Read Handlers​

With the TD defined, we connect it to the actual values the properties should return.

setPropertyReadHandler runs whenever a Consumer reads that property. It simply returns the current value of the matching variable.

thing.setPropertyReadHandler("coffeeBeansLeft", async () => coffeeBeansLeft);
thing.setPropertyReadHandler("waterLevel", async () => waterLevel);

Exposing the Thing​

The last step starts the server and makes the Thing reachable on the network.

thing.expose() exposes the Thing, making its interaction affordances reachable over the network. The full TD — with all forms filled in automatically — is served at the root URL.

thing.expose().then(() => {
console.log("Thing exposed on:");
console.log(" HTTP → http://localhost:8080/smart-coffee-machine");
});
note

node-wot derives the URL path from the title field: it lowercases the string and replaces spaces with hyphens. "Smart Coffee Machine" becomes smart-coffee-machine. The root URL always follows the pattern http://[host]:[port]/[sanitized-title].

View complete thing.js
const { Servient } = require("@node-wot/core");
const { HttpServer } = require("@node-wot/binding-http");

let coffeeBeansLeft = 320;
let waterLevel = 1000;

const servient = new Servient();
servient.addServer(new HttpServer({ port: 8080 }));

servient.start().then((WoT) => {
WoT.produce({
title: "Smart Coffee Machine",
description: "Remote controllable coffee machine",
id: "urn:uuid:0804d572-cce8-422a-bb7c-4412fcd56f06",
"@context": "https://www.w3.org/2022/wot/td/v1.1",
properties: {
coffeeBeansLeft: {
title: "Remaining Coffee Beans",
type: "number",
minimum: 0,
maximum: 500,
unit: "g",
readOnly: true,
observable: false,
},
waterLevel: {
title: "Water Level",
type: "number",
minimum: 0,
maximum: 1000,
unit: "ml",
readOnly: true,
observable: false,
}
}
}).then((thing) => {

thing.setPropertyReadHandler("coffeeBeansLeft", async () => coffeeBeansLeft);
thing.setPropertyReadHandler("waterLevel", async () => waterLevel);

thing.expose().then(() => {
console.log("Thing exposed on:");
console.log(" HTTP → http://localhost:8080/smart-coffee-machine");
});

});
});
View complete Thing Description

Both implementations serve the same TD. This is what a Consumer receives when it fetches http://localhost:8080/smart-coffee-machine:

{
"@context": "https://www.w3.org/2022/wot/td/v1.1",
"id": "urn:uuid:0804d572-cce8-422a-bb7c-4412fcd56f06",
"title": "Smart Coffee Machine",
"description": "Remote controllable coffee machine",
"properties": {
"coffeeBeansLeft": {
"title": "Remaining Coffee Beans",
"type": "number",
"minimum": 0,
"maximum": 500,
"unit": "g",
"readOnly": true,
"observable": false,
"forms": [
{
"href": "http://localhost:8080/smart-coffee-machine/properties/coffeeBeansLeft",
"contentType": "application/json",
"op": "readproperty"
}
]
},
"waterLevel": {
"title": "Water Level",
"type": "number",
"minimum": 0,
"maximum": 1000,
"unit": "ml",
"readOnly": true,
"observable": false,
"forms": [
{
"href": "http://localhost:8080/smart-coffee-machine/properties/waterLevel",
"contentType": "application/json",
"op": "readproperty"
}
]
}
}
}

Testing Your Properties​

Start the server:

node thing.js

You should see:

Thing exposed on:
HTTP → http://localhost:8080/smart-coffee-machine
OS differences

The curl commands below work on macOS, Linux, and Windows Git Bash. On Windows PowerShell, use Invoke-WebRequest or a GUI client.

tip

GUI HTTP clients like Postman, Bruno, and Insomnia are convenient alternatives to curl — paste the URL and inspect the response in a structured view.

First, fetch the TD to confirm it describes your properties correctly:

curl http://localhost:8080/smart-coffee-machine

Then read each property:

curl http://localhost:8080/smart-coffee-machine/properties/coffeeBeansLeft

Expected: 320

curl http://localhost:8080/smart-coffee-machine/properties/waterLevel

Expected: 1000

The values match the initial state declared in code. Notice that with node-wot you never wrote the property routes — the href in the TD was filled in automatically by node-wot based on the server port and property name. With Express you wrote the route and the href yourself, so they must stay in sync manually.

Summary​

You now have a running Thing that exposes two properties over HTTP and serves its own Thing Description. The TD is the entry point — any Consumer that fetches it learns the property names, data types, constraints, and the exact URLs to read them from.

In the next tutorial, we'll extend this Thing with the brewCoffee action and the lowOnWater event, completing all three interaction affordance types.