REST API over (Web)sockets using an Express style interface
- 100% native javascript
- Compatible with Websockets, Sockhop and socket.io
- Uses express-style controller declarations (see Asseverate if you want to write rest-over-sockets- and express-compatible controllers)
- Supports parameter capture, automatic error handling, and response encoding
You have an application whose only clients connect directly over TCP/IP
or
your clients all support Websockets/socket.io
and
you don't see the point in programming multiple API endpoints - some REST over HTTP, some over Websockets.
Check out the full documentation here
Incoming requests are simple native object, presumably transmitted over the wire using JSON. You can do this yourself, or you can use a library like Sockhop or socket.io -- depending on if you are in browser or not. Alternatively, you can use the ROSRequest object together with the .toJSON() method to ensure a correct format.
| Parameter | Type | Example | Required | Notes |
|---|---|---|---|---|
| method | string | "POST" | Y | Must be upper-case |
| path | string | "/photos/cat.jpg" | Y | |
| header | ?object | { "content-type": "application/json" } | N | Ought to be lower-case keys, though this is coerced on the server |
| body | ?object | { "some": "data" } | N | |
| params | object | { id: 23 } | RESERVED - auto populated from URL capture parameters | |
| query | object | { limit: 100 } | RESERVED - auto populated from URL capture parameters |
{
"method": "GET",
"path": "/apple/3444"
}If your handler throws an exception, the error will automatically result in a HTTP style 500 response. Routes that don't exist return a HTTP style 404 error.
The response body will look like this. Note that the headers are coerced to be lower-case, following the express.js convention for headers.
{
"statusCode": 200,
"headers": {
"content-type": "application/json"
},
"data": [
{
"type": "Apple",
"id": "23",
"attributes": {
"flavor": "sweet"
}
}
]
}Checkout the examples folder for the source code:
const wss=new (require("ws")).Server({ port: 8080 });
const restos=new (require("rest-over-sockets"))();
// Set up a server
wss.on("connection", (ws)=>{
ws.on("message", (message)=> {
restos.receive(JSON.parse(message),(response)=>{
ws.send(JSON.stringify(response));
});
});
});
// Add an Express-style route
restos.get("/widget/:id", (req, res)=>{
res
.set('Content-Type', 'text/json') // note, this gets coerced lower-case
.status(200)
.json([{ type:"Apple", id:req.params.id, attributes:{ flavor: "sweet" }}])
});const ws= new (require("ws"))("ws://localhost:8080/");
ws.on("open", ()=>{
ws.send(JSON.stringify({
method : "GET",
path : "/widget/23"
}));
});
ws.on("message", (data)=>{
console.log(data); // {"status":200,"headers":{"content-type":"text/json"},"data":[{"type":"Apple","id":"23","attributes":{"flavor":"sweet"}}]}
ws.close();
});Of course, Websockets has it's limitations, and so these days lots of people are using socket.io as a nice abstraction layer to handle these details. Rest-over-sockets integrates nicely into your pre-existing socket.io infastructure
const http = require('http');
const { Server:IOServer } = require('socket.io');
const server = http.createServer();
server.listen(3000, () => {
console.log("Listening on http://localhost:3000");
});
const io = new IOServer(server);
const restos=new (require("rest-over-sockets"))();
// Set up a server
io.on("connection", (sock)=>{
// Request will be emitted on "ROSRequest" by the ROSClient
sock.on("ROSRequest", (msg, callback)=> {
console.log("Received message", msg);
restos.receive(msg,callback);
});
});
// Add an Express-style route
restos.get("/widget/:id", (req, res)=>{
res
.set('Content-Type', 'text/json') // note, this gets coerced lower-case
.status(200)
.json([{ type:"Apple", id:req.params.id, attributes:{ flavor: "sweet" }}])
});const { ROSClient } = require("rest-over-sockets");
const { io } = require('socket.io-client');
const socket = io('http://localhost:3000');
const client = ROSClient.socketio(socket);
socket.on("connect", ()=>{
client.get("/widget/3444").then(response=>{
console.log(`Response: ${JSON.stringify(response)}`);
socket.disconnect();
});
});Socket.io is great, but sometimes you are using native (i.e. tcp or unix) sockets, In that case: try Sockhop, since it will automatically handle reconnections, remote callbacks to ensure the response is given to the request that called it, and also JSON encoding and possible packetization / fragmentation across the wire. Basically, it fixes all the nasty edge-cases of raw tcp sockets so that you can just focus on the real work of writing the interface.
const server=new (require("sockhop").server)();
const restos=new (require("rest-over-sockets"))();
server.listen();
// Assume everything that comes over the wire is a ROSRequest
server.on("receive", (o, meta)=>restos.receive(o, meta.callback));
restos.get("/apple/:id", (req, res)=>{
res
.set('Content-Type', 'text/json') // note, this gets coerced lower-case
.status(200)
.json([{ type:"Apple", id:req.params.id, attributes:{ flavor: "sweet" }}])
});const client=new (require("sockhop").client)();
client.connect().then(()=>{
client.send({
method: "GET",
path: "/apple/3444"
},(response)=>{
console.log(`Response: ${JSON.stringify(response)}`);
client.disconnect();
});
});const server=new (require("sockhop").server)();
const restos=new (require("rest-over-sockets"))();
server.listen();
server.on("request", (req,res,meta)=> {
// Request will be sent as a "ROSRequest" type by the ROSClient
if ( req.type !== "ROSRequest" ) return; // ignore other types
restos.receive(req.data, (obj) => res.send(obj))
});
restos.get("/apple/:id", (req, res)=>{
res
.set('Content-Type', 'text/json') // note, this gets coerced lower-case
.status(200)
.json([{ type:"Apple", id:req.params.id, attributes:{ flavor: "sweet" }}])
});const { ROSClient } = require("rest-over-sockets");
const sock=new (require("sockhop").client)();
const client = ROSClient.sockhop(sock);
sock.connect().then(()=>{
return client.get("/apple/3444")
}).then(response => {
console.log(`Response: ${JSON.stringify(response)}`);
sock.disconnect();
});Make sure your handlers (added by calling .get(), .post(), etc) run asynchronously. Example:
// BAD!
restos.get("/some/path", (req, res)=>{
NASTY_BLOCKING_TASK();
/* ... */
res.send();
});
// GOOD
restos.get("/some/path", (req, res)=>{
return new Promise((resolve)=>{
NASTY_BLOCKING_TASK();
/* ... */
resolve();
})
.then(res.send);
});
- Add cookie support?
- Support streaming?
- Support more content types?
- Default 404 and 500 message types
MIT