As you have seen, Nest.js provides a way to use web sockets into your app through the @nestjs/websockets package. Also, inside the framework the usage of the Adapter allows you to implement the socket library that you want.
By default, Nest.js comes with it’s own adapter, which allows you to use socket.io, a well known library for web sockets.
You have the possibility to create a full web socket app, but also, add some web socket features inside your Rest API. In this chapter, we will see how to implement the web socket over a Rest API using the decorators provided by Nest.js, but also how to validate an authenticated user using specific middleware.
The advantage of the web socket is to be able to have some real-time features in an application depending on your needs. For this chapter you can have a look at the /src/gateways files from the repository, but also /src/shared/adapters and /src/middlewares.
Imagine the following CommentGatewayModule, which looks like this:
@Module({imports:[UserModule,CommentModule],providers:[CommentGateway]})exportclassCommentGatewayModule{}
Import the UserModule in order to have access to the UserService, which will be useful later, as well as the CommentModule. Of course, we will create the CommentGateway, which is used as an injectable service.
To implement your first module using the Nest.js web socket, you will have to use the @WebSocketGateway decorator. This decorator can take an argument as an object to provide a way to configure how to use the adapter.
The implementation of the arguments respect the interface GatewayMetadata, allowing you to provide:
port, which must be use by the adapternamespace, which belongs to the handlersmiddlewares that have to be applied on the gateway before accessing the handlersAll the parameters are optional.
To use it, you have to create you first gateway class, so imagine a UserGateway:
@WebSocketGateway({middlewares:[AuthenticationGatewayMiddleware]})exportclassUserGateway{/*....*/}
By default, without any parameters, the socket will use the same port as your express server (generally 3000). As you can see, in the previous example we used a @WebSocketGateway, which uses the default port 3000 without namespace and with one middleware that we will see later.
The gateways in the class using the decorator previously seen contain all of the handlers that you need to provide the results of an event.
Nest.js comes with a decorator that allows you to access the server instance @WebSocketServer. You have to use it on a property of your class.
exportclassCommentGateway{@WebSocketServer()server;/* ... */}
Also, throughout the gateway, you have access to the injection of injectable services. So, in order to have access of the comment data, inject the CommentService exported by the CommentModule, which has been injected into this module.
exportclassCommentGateway{/* ... */constructor(privatereadonlycommentService:CommentService){}/* ... */}
The comment service allows you to return the appropriate result for the next handlers.
exportclassCommentGateway{/* ... */@SubscribeMessage('indexComment')asyncindex(client,data):Promise<WsResponse<any>>{if(!data.entryId)thrownewWsException('Missing entry id.');constcomments=awaitthis.commentService.findAll({where:{entryId:data.entryId}});return{event:'indexComment',data:comments};}@SubscribeMessage('showComment')asyncshow(client,data):Promise<WsResponse<any>>{if(!data.entryId)thrownewWsException('Missing entry id.');if(!data.commentId)thrownewWsException('Missing comment id.');constcomment=awaitthis.commentService.findOne({where:{id:data.commentId,entryId:data.entryId}});return{event:'showComment',data:comment};}}
We now have two handlers, the indexComment and the showComment.
To use the indexComment handler we expect an entryId in order to provide the appropriate comment, and for the showComment we expect an entryId, and of course a commentId.
As you have seen, to create the event handler use the @SubscribeMessage decorator provide by the framework. This decorator will create the socket.on(event) with the event corresponding to the string passed as a parameter.
We have set up our CommentModule, and now we want to authenticate the user using the token (have a look to the Authentication chapter). In this example we use a mutualised server for the REST API and the Websocket event handlers. So, we will mutualise the authentication token in order to see how to validate the token received after a user has been logged into the application.
It is important to secure the websocket in order to avoid the access of data without logging into the application.
As shown in the previous part, we have used middleware named AuthenticationGatewayMiddleware. The purpose of this middleware is to get the token from the web socket query, which is brought with the auth_token property.
If the token is not provided, the middleware will return a WsException, otherwise we will use the jsonwebtoken library
(have a look to the Authentication chapter) to verify the token.
Let’s set up the middleware:
@Injectable()exportclassAuthenticationGatewayMiddlewareimplementsGatewayMiddleware{constructor(privatereadonlyuserService:UserService){}resolve() {return(socket,next)=>{if(!socket.handshake.query.auth_token){thrownewWsException('Missing token.');}returnjwt.verify(socket.handshake.query.auth_token,'secret',async(err,payload)=>{if(err)thrownewWsException(err);constuser=awaitthis.userService.findOne({where:{payload.email}});socket.handshake.user=user;returnnext();});}}}
The middleware used for the web socket is almost the same as the REST API. Implementing the GatewayMiddleware interface with the resolve function is now nearly the same. The difference is that you have to return a function, which takes socket and the next function as its parameters. The socket contains the handshake with the query sent by the client, and all of the parameters provided, in our case, the auth_token.
Similar to the classic authentication middleware (have a look to the Authentication chapter), the socket will try to find the user with the given payload, which here contains the email, and then register the user into the handshake in order to be accessible into the gateway handler. This is a flexible way to already have the user in hand without finding it again in the database.
As mentioned in the beginning of this chapter, Nest.js comes with it own adapter, which uses socket.io. But the framework needs to be flexible and it can be used with any third party library. In order to provide a way to implement another library, you have the possibility to create your own adapter.
The adapter has to implement the WebSocketAdapter interface in order to implement the following methods. For example, we will use ws as a socket library in our new adapter. To use it, we will have to inject the app
into the constructor as follows:
exportclassWsAdapterimplementsWebSocketAdapter{constructor(privateapp:INestApplication){}/* ... */}
By doing this, we can get the httpServer in order to use it with the ws.
After that, we have to implement the create method in order to create the socket server.
exportclassWsAdapterimplementsWebSocketAdapter{/* ... */create(port:number){returnnewWebSocket.Server({server:this.app.getHttpServer(),verifyClient:({origin,secure,req},next)=>{return(newWsAuthenticationGatewayMiddleware(this.app.select(UserModule).get(UserService))).resolve()(req,next);}});}/* ... */}
As you can see, we have implemented the verifyClient property, which takes a method with { origin, secure, req } and
next values. We will use the req, which is the IncomingMessage from the client and the next method in order to
continue the process. We use the WsAuthenticationGatewayMiddleware to authenticate the client with the token, and to inject the appropriate dependencies we select the right module and the right service.
The middleware in this case processes the authentication:
@Injectable()exportclassWsAuthenticationGatewayMiddlewareimplementsGatewayMiddleware{constructor(privateuserService:UserService){}resolve() {return(req,next)=>{constmatches=req.url.match(/token=([^&].*)/);req['token']=matches&&matches[1];if(!req.token){thrownewWsException('Missing token.');}returnjwt.verify(req.token,'secret',async(err,payload)=>{if(err)thrownewWsException(err);constuser=awaitthis.userService.findOne({where:{payload.email}});req.user=user;returnnext(true);});}}}
In this middleware, we have to manually parse the URL to get the token and verify it with jsonwebtoken. After that, we have to implement the bindClientConnect method to bind the connection event to a callback that will
be used by Nest.js. It is a simple method, which takes in arguments to the server with a callback method.
exportclassWsAdapterimplementsWebSocketAdapter{/* ... */bindClientConnect(server,callback:(...args:any[])=>void){server.on('connection',callback);}/* ... */}
To finish our new custom adapter, implement the bindMessageHandlers in order to redirect the event and the data to the appropriate handler of your gateway. This method will use the bindMessageHandler in order to execute the handler and return the result to the bindMessageHandlers method, which will return the result to the client.
exportclassWsAdapterimplementsWebSocketAdapter{/* ... */bindMessageHandlers(client:WebSocket,handlers:MessageMappingProperties[],process:(data)=>Observable<any>){Observable.fromEvent(client,'message').switchMap((buffer)=>this.bindMessageHandler(buffer,handlers,process)).filter((result)=>!!result).subscribe((response)=>client.send(JSON.stringify(response)));}bindMessageHandler(buffer,handlers:MessageMappingProperties[],process:(data)=>Observable<any>):Observable<any>{constdata=JSON.parse(buffer.data);constmessageHandler=handlers.find((handler)=>handler.message===data.type);if(!messageHandler){returnObservable.empty();}const{callback}=messageHandler;returnprocess(callback(data));}/* ... */}
Now, we have created our first custom adapter. In order to to use it, instead of the Nest.js IoAdapter, we have to call the useWebSocketAdapter provided by the app: INestApplication in your main.ts file as follows:
app.useWebSocketAdapter(newWsAdapter(app));
We pass in the adapter, the app instance, to be used as we have seen in the previous examples.
In the previous section, we covered how to set up the web socket on the server side and how to handle the event from the client side.
Now we will see how to set up your client side, in order to use the Nest.js IoAdapter or our custom WsAdapter. In order to use the IoAdapter, we have to get the socket.io-client library and set up our first HTML file.
The file will define a simple script to connect the socket to the server with the token of the logged in user. This token we will be used to determine if the user is well connected or not.
Check out the following code:
<script>constsocket=io('http://localhost:3000',{query:'auth_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3QzQHRlc3QuZnIiLCJpYXQiOjE1MjQ5NDk3NTgsImV4cCI6MTUyNDk1MzM1OH0.QH_jhOWKockuV-w-vIKMgT_eLJb3dp6aByDbMvEY5xc'});</script>
As you see, we pass at the socket connection a token auth_token into the query parameter. We can pick it from the socket handshake and then validate the socket.
To emit an event, which is also easy, see the following example:
socket.on('connect',function(){socket.emit('showUser',{userId:4});socket.emit('indexComment',{entryId:2});socket.emit('showComment',{entryId:2,commentId:1});});
In this example, we are waiting for the connect event to be aware when the connection is finished. Then we send three events: one to get the user, then an entry, and the comment of the entry.
Using the following on event, we are able to get the data sent by the server as a response to our previously emitted events.
socket.on('indexComment',function(data){console.log('indexComment',data);});socket.on('showComment',function(data){console.log('showComment',data);});socket.on('showUser',function(data){console.log('showUser',data);});socket.on('exception',function(data){console.log('exception',data);});
Here we show in the console all of the data responded by the server, and we have also implemented an event exception in order to catch all exceptions that the server can return.
Of course, as we have seen in the authentication chapter, the user can’t access the data from another user.
In cases where we would like to use the custom adapter, the process is similar.
We will open the connection to the server using the WebSocket as follows:
constws=newWebSocket("ws://localhost:3000?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3QzQHRlc3QuZnIiLCJpYXQiOjE1MjUwMDc2NjksImV4cCI6MTUyNTAxMTI2OX0.GQjWzdKXAFTAtOkpLjId7tPliIpKy5Ru50evMzf15YE");
We open the connection here on the localhost with the same port as our HTTP server. We also pass the token as a query
parameter in order to pass the verifyClient method, which we have seen with the WsAuthenticationGatewayMiddleware.
Then we will wait for the return of the server, to be sure that the connection is successful and usable.
ws.onopen=function(){console.log('open');ws.send(JSON.stringify({type:'showComment',entryId:2,commentId:1}));};
When the connection is usable, use the send method in order to send the type of event we want to handle, which is here with showComment and where we pass the appropriate parameters, just like we did with the socket.io usage.
We will use the onmessage in order to get the data returned by the server for our previous sent event. When the WebSocket receives an event, a message event is sent to the manager that we can catch with the following example.
ws.onmessage=function(ev){const_data=JSON.parse(ev.data);console.log(_data);};
You can now use this data as you’d like in the rest of your client app.
In this chapter you learned how to set up the server side, in order to use the:
socket.io library provided by the Nest.js IoAdapterws library with a custom adapterYou also set up a gateway in order to handle the events sent by the client side.
You’ve seen how to set up the client side to use the socket.io-client or WebSocket client to connect the socket to the server. This was done on the same port as the HTTP server, and you learned how to send and catch the data returned by the server or the exception in case of an error.
And finally, you learned how to set up the authentication using middleware in order to check the socket token provided and identify if the user is authenticated or not to be able to access the handler in the cases of an IoAdapter or a custom adapter.
The next chapter will cover microservices with Nest.js.