Skip to content
On this page

Server guide

Installation

bash
npm install @jcbuisson/express-x

Creating an express-x server

js
// 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:

js
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 relayed to the prisma instance; on the client:

  1. the call will be serialized and sent to the server through the websocket connection
  2. it will be executed on the server
  3. 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:

js
// 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 })
      },
   })
}
js
// app.js
...
import mailService from '/src/services/custom/mail/mail.service.js'
...
app.configure(mailService) // equivalent to: mailService(app)
...
js
// 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':

js
// 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:

js
const updatedUser = await app.service('user').update({
   where: { id: 123 },
   data: { firstname: "..." }
})

Service hooks

Hooks are functions which are called during the lifecycle of a service method call. For exemple :

js
// /src/services/database/user/user.service.js
import hooks from './user.hooks.js'

export default function (app) {
   findUnique: prisma.User.findUnique,
   findMany: prisma.User.findMany,
   create: prisma.User.create,
   update: prisma.User.update,
   delete: prisma.User.delete,
   app.createService('user', methods)
   
   app.service('user').hooks(hooks)
}
js
// /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:

js
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],
   },
}

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 connection
  • context.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.

js
// 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-wide events

The server can send events on its own 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) => {
   ...
})