Server guide
Installation
npm install @jcbuisson/express-x
Creating an express-x server
// app.js
import { expressX } from '@jcbuisson/express-x'
// `app` is a regular express application, enhanced with express-x services and real-time features
const options = { debug: true }
const app = expressX(options)
// add services, express middleware, ...
...
app.httpServer.listen(8000, () => console.log(`App listening at http://localhost:8000`))
Services
An ExpressX application is mostly composed of services. A service has a name and is composed of a set of methods of any names and signatures. Here is an example of the call of a method send
of a service mail
:
const result = await app.service('mail').send(from, to, subject, text)
ExpressX is isomorphic: this call can be written in the server code or in the client code, using exactly the same syntax.
On the server, the call will be directly executed; on the client:
- the call will be serialized and sent to the server through the websocket connection
- it will be executed on the server
- the returned value (or an error) will be sent back to the client
Moreover, the pub/sub mechanism may send an associated event with the result to subscriber clients.
Custom services
Here is an implementation of the service mail
exposing the send
method:
// src/services/custom/mail/mail.service.js
import nodemailer from 'nodemailer'
import config from 'config'
export default function (app) {
app.createService('mail', {
send: (from, to, subject, text) => {
const transporter = nodemailer.createTransport(config.NODEMAILER)
return transporter.sendMail({ from, to, subject, text })
},
})
}
// app.js
...
import mailService from '/src/services/custom/mail/mail.service.js'
...
app.configure(mailService) // equivalent to: mailService(app)
...
// client.js
...
const result = await app.service('mail').send('me@mail.org', 'you@mail.org', "The subject", "The message")
Database services
ExpressX allows to easily create services for the tables of a database. For example, if User
is the name of a model in a Prisma schema, the following statement will create an associated service named 'user':
// app.js
...
const methods = {
create: prisma.User.create,
update: prisma.User.update,
delete: prisma.User.delete,
}
app.createService('user', methods)
With these simple statements, Prisma methods 'create', 'update', 'delete' become accessible from app.service('user')
, on the server as well as on the client.
For example:
const updatedUser = await app.service('user').update({
where: { id: 123 },
data: { firstname: "..." }
})
With Prisma, since all methods 'create', 'update', etc. on a table such as 'User' are attached to prisma.User, we can simply write:
app.createService('user', prisma.User)
Hooks
Hooks are functions which are called during the lifecycle of a service method call. For exemple :
// /src/services/database/user/user.service.js
import hooks from './user.hooks.js'
export default function (app) {
app.createService('user', prisma.user)
app.service('user').hooks(hooks)
}
// /src/services/database/user/user.hooks.js
import { protect } from '@jcbuisson/express-x'
export default {
after: {
all: [protect('password')]
}
}
protect
is a hook provided by ExpressX which removes a particular field (here: 'password') in the result of a method service call, garanteeing that the caller will never get the password in a user record. Since the keyword 'all' is used, the hook is called for every method.
It is easy to write a custom hook; it usually requires to handle the context object. For example:
async function addAuthorArgument(context) {
// extract userId from connection data (we suppose it has been set on login)
const authorId = context.socket.data.userId
// add new arguments to method
context.args.push(authorId)
}
export default {
before: {
mymethod: [addAuthorArgument],
},
}
Common hooks provided by ExpressX
addTimestamp(field)
: add current date to attributefield
on the resulthashPassword(passwordField)
: hash propertyfield
protect(field)
: removefield
from result
Context
context
is an object passed as an argument of every hook. It provides information on the call and its arguments, on the caller, on the constructed result value:
context.app
: Object
The underlying ExpressX (and Express) application
context.caller
: String
It is 'client' for a client-side call, 'server' for a server-side call
context.serviceName
: String
The service name
context.methodName
: String
The service method name
context.args
: Array
The list of arguments of the call
context.result
: any
Contains the result of the call. It may be modified by 'after' hooks.
context.socket
: Socket
The socket.io object representing the persistent connection between the server and the client.
context.socket.id
is a unique id for this connectioncontext.socket.data
contains arbitrary data, typically used to store information about authentication and authorisation.context.socket.rooms
is the set of all rooms the socket is currently in
Publish / subscribe
Channels
Channels are used for the pub/sub mechanism. Service methods publish events on channels, and clients subscribe to channels in order to receive those events. ExpressX provides functions to configure which events are published to which channels. A channel is represented by a name and you can create and use as many channels as you need.
In the following example, every time a client connects to the server, it joins (= is subscribed to) the 'anonymous' channel.
// app.js
import { expressX } from '@jcbuisson/express-x'
const app = expressX()
...
// subscribe
app.on('connection', (socket) => {
app.joinChannel('anonymous', socket)
})
app.server.listen(8000, () => console.log(`App listening at http://localhost:8000`))
Service events
Whenever an event is emited by a service, it is published on all channels for which the caller subscribed, thus broacasted to all connected clients belonging to these channels. This mechanism is typically used to provide real-time updates in a web or mobile application. For example, when the backend of a medical application updates the record of a patient, all clients of the medical staff will receive the update event if they subscribed to a channel associated to the user.
See here for handling these service events on the client side.
Application events
The server can send events on a channel at any moment, independently of any service method call:
app.sendAppEvent('mychannel', 'myeventtype', args...)
See here for handling these events on the client side.
Connection events
Listeners can be added to handle the lifecycle events during a connection between a client and the server:
// on client connection
app.addConnectListener((socket) => {
...
})
// before client disconnects
app.addDisconnectingListener((socket, reason) => {
...
})
// when client is disconnected
app.addDisconnectListener((socket, reason) => {
...
})