Applications can generate a lot of events when they're running. However, most of the time, the only way to know what's going on is by looking at the logs or running queries against the database. It would be nice to let the users see what is going on in an easy way, so why not build an activity feed to see in realtime, every change made to the models of the application?
In this tutorial we are going to build a simple Node.js REST API with Express and Mongoose to work with generic measurements, let's say for example, temperatures. Every time a database record is modified (created/updated/deleted), it will trigger an event to a channel in realtime using Pusher Channels. In the frontend, those events will be shown in an activity feed made with React.
This is how the final application will look like:
This tutorial assumes prior knowledge of Node.js and React. We will integrate Pusher into a Node.js API, create React components and hook them up with Pusher. However, since Pusher is so easy to use together with Node.js and React, you might feel that in this tutorial we will spend most of our time setting things up in the backend and creating the React components.
You'll need to have access to a MongoDB database. If you're new to MongoDB, you might find this documentation on how to install it handy.
The source code of the final version of the application is available on Github.
The project has the following structure:
1|— models 2| |— measure.js 3|— public 4| |— css 5| |— images 6| |— js 7| | |— app.js 8| | |— event.js 9| | | — events.js 10| | |— header.js 11|— routes 12| |— api.js 13| |— index.js 14|— views 15| |— index.ejs 16|- package.json 17|- server.js
model
directory contains the Mongoose schema to interact with the database.public
directory contains the CSS and images files as well as the Javascript (React) files that will be used on the main web page of the app.routes
directory contains the server's API endpoints and the route to server the main page of the app.view
directory contains the EJS template for the main page of the app.Create a free account at Pusher.
When you first log in, you'll be asked to enter some configuration options:
Enter a name, choose React as your frontend tech, and Node.js as your backend tech. This will give you some sample code to get you started.
This won't lock you into a specific set of technologies, you can always change them. With Channels, you can use any combination of libraries.
Then go to the App Keys tab to copy your App ID, Key, and Secret credentials, we'll need them later.
First, add a default package.json
configuration file with:
npm init -y
For running the server, we'll need Express, React, Pusher, and other dependencies, let's add them with:
npm install --save express ejs body-parser path pusher mongoose
Here are the dependencies section on the package.json file in case a future version of a dependency breaks the code:
1{ 2 ... 3 "dependencies": { 4 "body-parser": "^1.15.2", 5 "ejs": "^2.5.2", 6 "express": "^4.14.0", 7 "mongoose": "^4.6.4", 8 "path": "^0.12.7", 9 "pusher": "^1.5.0", 10 } 11}
The backend is a standard Express app with Mongoose to interact with the database. In the server.js file, you can find the configuration for Express:
1var app = express(); 2 3app.use(bodyParser.json()); 4app.use(bodyParser.urlencoded({ extended: true })); 5 6app.use(express.static(path.join(__dirname, 'public'))); 7 8app.set('views', path.join(__dirname, 'views')); 9app.set('view engine', 'ejs');
The routes exposed to the server are organized in two different files:
1app.use('/', index); 2app.use('/api', api);
Then, the app will connect to the database and start the web server on success:
1mongoose.connect('mongodb://localhost/temperatures'); 2 3var db = mongoose.connection; 4db.on('error', console.error.bind(console, 'Connection Error:')); 5db.once('open', function () { 6 app.listen(3000, function () { 7 console.log('Node server running on port 3000'); 8 }); 9});
However, the interesting part is in the file routes/api.js. First, the Pusher object is created passing the configuration object with the App ID, the key, and the secret for the Pusher app:
1var pusher = new Pusher({ 2 appId : process.env.PUSHER_APP_ID, 3 key : process.env.PUSHER_APP_KEY, 4 secret : process.env.PUSHER_APP_SECRET, 5 encrypted : true, 6});
Pusher can be used to publish any events that happen in our application. These events have a channel, which allows events to relate to a particular topic, an event-name used to identify the type of the event, and a payload, which you can attach any additional information to the message.
We are going to publish an event to a Pusher channel when a database record is created/updated/deleted with that record as attachment so we can show it in an activity feed.
Here's the definition of our API's REST endpoints. Notice how the event is triggered using pusher.trigger
after the database operation is performed successfully:
1/* CREATE */ 2router.post('/new', function (req, res) { 3 Measure.create({ 4 measure: req.body.measure, 5 unit: req.body.unit, 6 insertedAt: Date.now(), 7 }, function (err, measure) { 8 if (err) { 9 ... 10 } else { 11 pusher.trigger( 12 channel, 13 'created', 14 { 15 name: 'created', 16 id: measure._id, 17 date: measure.insertedAt, 18 measure: measure.measure, 19 unit: measure.unit, 20 } 21 ); 22 23 res.status(200).json(measure); 24 } 25 }); 26}); 27 28router.route('/:id') 29 /* UPDATE */ 30 .put((req, res) => { 31 Measure.findById(req.params.id, function (err, measure) { 32 if (err) { 33 ... 34 } else if (measure) { 35 measure.updatedAt = Date.now(); 36 measure.measure = req.body.measure; 37 measure.unit = req.body.unit; 38 39 measure.save(function () { 40 pusher.trigger( 41 channel, 42 'updated', 43 { 44 name: 'updated', 45 id: measure._id, 46 date: measure.updatedAt, 47 measure: measure.measure, 48 unit: measure.unit, 49 } 50 ); 51 52 res.status(200).json(measure); 53 }); 54 55 } else { 56 ... 57 } 58 }); 59 }) 60 61 /* DELETE */ 62 .delete((req, res) => { 63 Measure.findById(req.params.id, function (err, measure) { 64 if (err) { 65 ... 66 } else if (measure) { 67 measure.remove(function () { 68 pusher.trigger( 69 channel, 70 'deleted', 71 { 72 name: 'deleted', 73 id: measure._id, 74 date: measure.updatedAt ? measure.updatedAt : measure.insertedAt, 75 measure: measure.measure, 76 unit: measure.unit, 77 } 78 ); 79 80 res.status(200).json(measure); 81 }); 82 } else { 83 ... 84 } 85 }); 86 });
Measure
is the Mongoose schema used to access the database. You can find its definition in the models/measure.js file:
1var measureSchema = new Schema({ 2 measure: { type: Number }, 3 insertedAt: { type: Date }, 4 updatedAt: { type: Date }, 5 unit: { type: String }, 6});
This way, we'll be listening to these events to update the state of the client in the frontend.
React thinks of the UI as a set of components, where you simply update a component's state, and then React renders a new UI based on this new state updating the DOM for you in the most efficient way.
The app's UI will be organized into three components, a header (Header
), a container for events (Events
), and a component for each event (Event
):
The template for the index page is pretty simple. It just contains references to the CSS files, a div
element where the UI will be rendered, the Pusher app key (passed from the server), and references to all the Javascript files the application uses:
1<!DOCTYPE html> 2<html> 3<head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1"> 6 <title>Realtime Activity Feed with Pusher + React</title> 7 <link rel="stylesheet" href="/css/all-the-things.css"> 8 <link rel="stylesheet" href="/css/style.css"> 9</head> 10 11<body class="blue-gradient-background"> 12 13 <div id="app"></div> 14 15 <!-- React --> 16 <script src="https://unpkg.com/react@15.3.2/dist/react-with-addons.js"></script> 17 <script src="https://unpkg.com/react-dom@15.3.2/dist/react-dom.js"></script> 18 <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script> 19 20 <!-- Libs --> 21 <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.15.2/moment.min.js"></script> 22 <script src="https://js.pusher.com/3.2/pusher.min.js"></script> 23 24 <!-- Pusher Config --> 25 <script> 26 var PUSHER_APP_KEY = '<%= pusher_app_key %>'; 27 </script> 28 29 <!-- App/Components --> 30 <script type="text/babel" src="/js/header.js"></script> 31 <script type="text/babel" src="/js/event.js"></script> 32 <script type="text/babel" src="/js/events.js"></script> 33 <script type="text/babel" src="/js/app.js"></script> 34 35</body> 36</html>
The application will be rendered in the div
element with the ID app
. The file public/js/app.js is the starting point for our React app:
1var App = React.createClass({ 2 ... 3}); 4 5ReactDOM.render(<App />, document.getElementById("app"));
Inside the App
class, first, we define our state as an array of events:
1var App = React.createClass({ 2 3 getInitialState: function() { 4 return { events: [] }; 5 }, 6 7 ... 8 9});
Then, we use the componentWillMount
method, which is invoked once immediately before the initial rendering occurs, to set up Pusher:
1var App = React.createClass({ 2 3 ... 4 5 componentWillMount: function() { 6 this.pusher = new Pusher(PUSHER_APP_KEY, { 7 encrypted: true, 8 }); 9 this.channel = this.pusher.subscribe('events_to_be_shown'); 10 }, 11 12 ... 13}); 14 15...
We subscribe to the channel's events in the componentDidMount
method and unsubscribe from all of them and from the channel in the componentWillUnmount
method:
1var App = React.createClass({ 2 3 ... 4 5 componentDidMount() { 6 this.channel.bind('created', this.updateEvents); 7 this.channel.bind('updated', this.updateEvents); 8 this.channel.bind('deleted', this.updateEvents); 9 } 10 11 componentWillUnmount() { 12 this.channel.unbind(); 13 14 this.pusher.unsubscribe(this.channel); 15 } 16 17 ... 18}); 19 20...
The updateEvents
function updates the state of the component so the UI can be re-render. Notice how the new event is prepended to the existing array of events. Since React works best with immutable objects, we create a copy of that array to then update this copy:
1var App = React.createClass({ 2 3 ... 4 5 updateEvents: function(data) { 6 var newArray = this.state.events.slice(0); 7 newArray.unshift(data); 8 9 this.setState({ 10 events: newArray, 11 }); 12 }, 13 14 ... 15}); 16 17...
Finally, the render
method shows the top-level components of our app, Header
and Events
:
1var App = React.createClass({ 2 3 ... 4 5 render() { 6 return ( 7 <div> 8 <Header /> 9 <Events events={this.state.events} /> 10 </div> 11 ); 12 } 13 14 ... 15} 16 17...
public/javascript/header.js is a simple component without state or properties that only renders the HTML for the page's header.
The Events
component (public/javascript/events.js) takes the array of events to create an array of Event
components:
1var Events = React.createClass({ 2 render: function() { 3 var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; 4 5 var eventsLength = this.props.events.length; 6 var eventsMapped = this.props.events.map(function (evt, index) { 7 const key = eventsLength - index; 8 return <Event event={evt} key={key} /> 9 }); 10 11 return <section className={'blue-gradient-background intro-splash splash'}> 12 <div className={'container center-all-container'}> 13 <h1 className={'white light splash-title'}> 14 Realtime Activity Feed with Pusher + React 15 </h1> 16 <ReactCSSTransitionGroup component="ul" className="evts" transitionName="evt-transition" transitionEnterTimeout={500} transitionLeaveTimeout={500}> 17 {eventsMapped} 18 </ReactCSSTransitionGroup> 19 </div> 20 </section>; 21 } 22});
There are two important things in this code.
First, React requires every message component in a collection to have a unique identifier defined by the key
property. This help it to know when elements are added or removed. As new elements are prepended instead of appended, we can't give the first element the index 0
as key since this will only work the first time an element is added (for the next added elements, there will be an element with key 0
already). Therefore, keys are assigned this way:
var key = eventsLength - index;
The second thing is that the insertion of a new event is done with the ReactCSSTransitionGroup
add-on component, which wraps the elements you want to animate. By default, it renders a span
to wrap them, but since we're going to work with li
elements, we specify the wrapper tag ul
with the component
property. className
becomes a property of the rendered component, as any other property that doesn't belong to ReactCSSTransitionGroup
.
transitionName
is the prefix used to identify the CSS classes to perform the animation. You can find them in the file public/css/style.css:
1.evt-transition-enter { 2 opacity: 0.01; 3} 4 5.evt-transition-enter.evt-transition-enter-active { 6 opacity: 1; 7 transition: opacity 500ms ease-in; 8} 9 10.evt-transition-leave { 11 opacity: 1; 12} 13 14.evt-transition-leave.evt-transition-leave-active { 15 opacity: 0.01; 16 transition: opacity 500ms ease-in; 17}
Finally, the Event
component (public/js/event.js), using Moment.js to format the date, renders the event in the following way:
1var Event = React.createClass({ 2 render: function() { 3 var name = this.props.event.name; 4 var id = this.props.event.id; 5 var date = moment(this.props.event.date).fromNow(); 6 var measure = this.props.event.measure; 7 var unit = this.props.event.unit; 8 9 return ( 10 <li className={'evt'}> 11 <div className={'evt-name'}>{name}:</div> 12 <div className={'evt-id'}>{id}</div> 13 <div className={'evt-date'}>{date}</div> 14 <div className={'evt-measure'}>{measure}°{unit}</div> 15 </li> 16 ); 17 } 18 });
To run the server, execute the server.js
file using the following command:
PUSHER_APP_ID=<YOUR PUSHER APP ID> PUSHER_APP_KEY=<YOUR PUSHER APP KEY> PUSHER_APP_SECRET=<YOUR PUSHER APP SECRET> node server.js
To test the whole app, you can use something to call the API endpoints with a JSON payload, like curl or Postman:
Or if you only want to test the frontend part with Pusher, you can use the Pusher Debug Console on your dashboard:
In this tutorial, we saw how to integrate Pusher into a Node.js backend and a React frontend. As you can see, it is trivial and easy to add Pusher to your app and start adding new features.
Remember that if you get stuck, you can find the final version of this code on Github or contact us with your questions.