How to create Twitter

“Learning by doing” is one of my favourite methods to learn anything. If you want to learn to build a social network, you should build a social network.

Let’s build Twitter, with an awful UI, because I’m not that good at CSS.

Our stack for this will be Node-based, so Express, etc. Let’s make our directory and install the packages we’ll be using.

mkdir twttr && cd twttr
npm init
npm install --save express express-session pug morgan body-parser mongoose

Express is our web server, the session package will manage sessions, pug is our templating language, morgan is a cute little module for prettier logging, body-parser allows us to work with body requests, so we can do form stuff, and mongoose allows us to work with MongoDB, a noSQL database.

Once these are all installed, let’s create all the required files and get to work.

touch index.js
mkdir public views

index.js is our main app file, public is our front-end files (HTML/CSS/JS), and views are our templates (.pug files that’ll be rendered).

Before any of this, we need to run mongoDB. Let’s do that by installing it and running its daemon.

brew install mongodb
mongod

Keep that terminal window open and seperate– let’s get to work on index.js.

// index.js
var express = require('express'),
session = require('express-session'),
bodyParser = require('body-parser'),
morgan = require('morgan'),
mongoose = require('mongoose'),
pug = require('pug');
var app = express();
var db = mongoose.connect('mongodb://localhost:27017/twttr');
var port = process.env.PORT || 3030;
app.get('/', (req, res) => {
res.send('welcome to twttr!');
});
app.listen(port);

Here, we’re requiring/importing all the packages we’ve installed and setting our app to an instance of express for ease of use, connecting to our mongoDB, defining our port (almost any four numbers work). app.get() lets us create a route for the root of the page, and res.send() lets us access the res object to send a string back to the user. Lastly, app.listen() watches the port we supply, so we can see it in action.

npm install -g nodemon # nodemon is node + it restarts on every change
nodemon index.js # open your browser and browse localhost:3030

You should see “welcome to twttr” on your web browser, in Times New Roman at the top left of the page, basking in all its glory. Congratulations.

Before we jump into anything, let’s get some configuration set up. Right under var port, let’s add a few lines.

// index.js
app.set('view engine', 'pug');
app.use(session({ secret: "lkjshd", saveUninitialized: true, resave: true }));
app.use(express.static('public'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(morgan('dev'));

On the first line we’re telling our app that we want to use pug as the view engine, or the templating language, to render our pages. The second line is setting a session with a secret token (which can be any random string, the longer it is the more secure it is. This prevents users from creating their own sessions). The third line sets the static folder to public, which means when we browse /css/style.css we can access /public/css/style.css. The fourth and fifth line encode content through bodyParser and let us access it with JSON. The last line sets morgan to dev mode so we can see all HTTP methods in our terminal when we’re running our app.

If we refresh our page we shouldn’t see any changes yet. Let’s change that.

In views, create two files, index.pug and layout.pug. First, let’s edit layout. Pug documentation is pretty simple to learn, and it’s practically HTML without tag brackets. Remember, indentation is important when closing tags don’t exist (Python flashbacks).

// views/layout.pug
doctype
html
head
title twttr
meta(charset="utf-8")
link(rel="stylesheet", href="/css/style.css")
body
h2 twttr
hr
  block body

block body denotes where we want our templating to stop and where our custom files to replace. In this case, we want everything inside body to be custom, except for h2 twttr and hr, because those are going to be at the top of every page. layout.pug will be used with every page you include it with (with extends layout).

In index.pug, we’re not going to do much yet, just get things set up.

// views/index.pug
extends layout
block body
h2 whats up boi

First, extends layout copies the file from layout.pug into our current file and let’s us work with it. block body, again, denotes where the custom work should come in. Inside it we have an h2 element. How nice.

Last but not least, let’s make this render at /. Open index.js and replace res.send('welcome to twttr!')with res.render('index'). res's render method renders the file specified in the views folder. It turns pug into HTML, instead of just returning a string.

Save your changes and open your browser to see your work.

home page boi

As you can see, we don’t have a lot so far, but at this point everything is very customizable and we can now add as many files as we want and call include them with extends layout.

Next thing we’ll have to do is make some models. Think of models as blueprints for different objects we’ll be dealing with. This is a really abstract concept to explain, so don’t worry if you don’t completely understand it. We need to create a model for anything that’s touching our database, and in this case that’s two things, users and tweets. Let’s make a models folder and create those models. Name models as singular objects (User, Tweet, Person, etc).

mkdir models
touch models/User.js
touch models/Tweet.js

Both of these files are going to be similar with a few differences. Let’s write one out. First, we have to install bcrypt, which lets us hash the passwords our users provide, since saving plaintext passwords in a database should never happen, ever.

// models/User.js
var mongoose = require('mongoose'),
bcrypt = require('bcrypt');
var userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
created_at: Date,
});
userSchema.pre('save', function(next) {
var user = this;
// new user gets current created at day
if (!this.created_at) this.created_at = new Date();
bcrypt.genSalt(10, (e, salt) => {
if (e) return next(e);
// creates hash with generated salt and stores it in password field
bcrypt.hash(user.password, salt, (err, hash) => {
user.password = hash;
next();
});
});
});
module.exports = mongoose.model('User', userSchema);

In this file, we include mongoose again, since we need it to define schemas (models), as well as bcrypt, so we can hash passwords when they’re saved. We define this in the model so we can use the pre('save') method, so that plaintext passwords never touch the database, and are hashed before they’re saved. Underneath the requirements, we define our user schema with 3 properties, username, password, and created_at. These are the three most fundamental properties any user needs to have (and it doesn’t even need to have created_at, to be fair). user.Schema.pre('save') lets us set created_at to the current date/time, and then set the user’s password field to the hash of the password provided. We also generate a salt with 10 rounds– the more rounds the more times the algorithm runs, which somewhat improves security, but the longer it takes, so we can stick to 10.

Let’s copy and paste this file into Tweet.js and change a few lines.

// models/Tweet.js
var mongoose = require('mongoose'),
bcrypt = require('bcrypt');
var tweetSchema = new mongoose.Schema({
content: { type: String, required: true },
author: { type: mongoose.Schema.ObjectId, required: true },
created_at: Date
});
tweetSchema.pre("save", function(next) {
if (!this.created_at) this.created_at = new Date();
next();
});
module.exports = mongoose.model('Tweet', tweetSchema);

Alright, cool, now we have our models defined. Phew. Let’s make the login/register pages.

touch views/login.pug
touch views/register.pug

Let’s dive into login first.

// views/login.pug
extends layout
block body
h3 login
form(action="/login", method="post")
input(type="text", name="username", placeholder="username")
input(type="password", name="password", placeholder="*****")
input(type="submit", value="Login")

Again, we’re using layout.pug, so we extend it. Then we create an h3 and form, which is going to POST /login with our body information. The name attribute of each input will be the value we access the content in JSON.

Let’s make register.pug the same, except replace the action with /register, as well as the h3. Cool, now let’s view them by creating a route for both.

Open index.js and underneath the route we defined for root, let’s add two, one for login and one for register.

// routes
app.get('/', (req, res) => {
res.render('index');
});
app.get('/login', (req, res) => {
res.render('login');
});
app.get('/register', (req, res) => {
res.render('register');
});

Save and open your browser to /login and /register, both of them should show the forms we created. Now let’s get to dealing with that content.

Underneath the get routes, let’s create a seperate post route for login and register. For now, let’s just res.send(req.body), which will send the contents of the form bodies to the page.

app.post('/login', (req, res) => {
res.send(req.body);
});
app.post('/register', (req, res) => {
res.send(req.body);
});

Try submitting the form on both pages and you should see the contents of the form returned.

JSON returned!

With this information we can now register a user. First, let’s require our models for User and Tweet right under the others. Remember, our models are local, so we need to use ./. Also, capitalize the model variables, it looks nice and it’s considered standard.

var express = require('express'),
session = require('express-session'),
bodyParser = require('body-parser'),
morgan = require('morgan'),
mongoose = require('mongoose'),
pug = require('pug');
var User = require('./models/User');
var Tweet = require('./models/Tweet');

Now jump down to the POST route for register. Let’s create a user.

// index.js
app.post('/register', (req, res) => {
var user = new User({
username: req.body.username,
password: req.body.password
});
var session = req.session;
user.save(err => {
if (err) {
res.redirect('/register');
} else {
console.log('created user: ' + user.username);
session._id = user._id;
return res.redirect('/@' + user.username);
}
});
});

This creates a new user from req.body, using our model, saves it–redirecting back to register if an error occurs, and redirects to @username if all goes well. Also, res.locals.user will set the local @username goes nowhere right now, so let’s just create a page that shows @username’s information.

First, create a new view.

touch views/profile.pug

Let’s have the user’s username and time of signup shown.

// profile.pug
extends layout
block body
h3 #{user.username}
p #{user.created_at}

All of this is familiar except the #{}, which is just Pug’s way of evaluating variables. Cool, let’s create a route for this, too. You can feel the repetitiveness of this, I know I can.

// index.js
app.get('/@:username', (req, res) => {
User.findOne({username: req.params.username}, (err, user) => {
res.render('profile', {user: user});
});
});

This one’s a bit new. First, in the app.get, we have :username, which just means it’s a URL variable/parameter. We want to do this because we have multiple users, and we want a page for each/any of them. The next new thing is User.findOne({}, ()), which is just a method our models come packed with that let us query them in our database. findOne lets us, you guessed it, find one (user), given the parameters we provide it– the first argument being a hash of what we want to filter by (in this case, we have the username so we’ll use that). It’s callback returns the user object with that username. Last thing, with res.render, we provide a second parameter here, a hash. These are variables we want to provide the file we’re rendering. This is so profile.pug knows what to populate #{user.username} with.

If you browse /@username you should see your account’s information.

we done did it

Let’s add login/register links at the top really quickly. Open views/layout.pug.

// views/layout.pug
doctype
html
head
title twttr
meta(charset="utf-8")
link(rel="stylesheet", href="/css/style.css")
body
h2
a(href="/") twttr
a(href="/login") Login
a(href="/register") Register
hr
block body

We just added login/register links here under the header, as well as made the “twttr” text link to /. Nothing special. Now we can navigate around the site.

Oh, really quick– let’s do a 404 page, it’s gonna be really simple.

Add this just above your app.listen.

app.get('*', (req, res) => {
res.send('you got an error :(');
});

This gets all routes that haven’t been dealt with already. All we’re doing here is sending “you got an error”. Error handling is for another day.

Now let’s do logging in/out.

Inside the POST route for login, add the following.

// index.js
app.post('/login', (req, res, next) => {
if (!req.body.username || !req.body.password) return next();
var session = req.session;
// find the user from the username login
User.findOne({username: req.body.username}, (e, user) => {
// make sure our passwords match
bcrypt.compare(req.body.username, user.password, (err, correct) => {
if (!correct) return next();
// the user is good to go :)
session._id = user._id;
return res.redirect('/@' + user.username);
});
});
});

Whoa, ok. First off, we added “next” to our parameters, which in this context means “exit this route and find the next one that applies to us”, which in this case would be app.get('*'), meaning it’ll tell us we have an error. Next off, we’re finding the user based on the username supplied in the login form. Then we take this user and compare its hashed password with the password supplied in the form using bcrypt’s compare method, which takes a callback with two parameters, an error and a boolean– the boolean being whether or not the passwords matched. If they don’t, we’ll just return next() again, but if they do match we’ll set session._id to the ID of the user, so we can access who’s logged in at any time. To test this this theory, let’s create a settings page for the authenticated user, which should only be available him/her.

Also, we should make sure only logged in users can see certain things, such as /settings. Thankfully, Express lets use write our own middleware really easily, so let’s do that right now.

At the bottom of index.js above app.listen, add this function.

function authenticated(req, res, next) {
if (req.session._id) return next();
 return res.redirect('/login');
}

Add this as a second parameter to any route you want to only be accessed by an authenticated user.

Create views/settings.pug. Let’s open it up.

// views/settings.pug
extends layout
block body
p this page should only be available to @#{user.username}

form(method="post", action="/settings")
input(type="text", value=user.username, placeholder="username", name="username")
input(type="submit", value="Save")

This page will let us change the user’s username, which we only want the user to be able to do. Let’s create a GET and POST for this in index.js.

// index.pug
app.get('/settings', authenticated, (req, res) => {
User.findOne({_id: req.session._id}, (err, user) => {
res.render('settings', {user: user});
});
});
app.post('/settings', authenticated, (req, res) => {
User.findOne({_id: req.session._id}, (err, user) => {
user.username = req.body.username;
user.save((err, user) => {
if (err) return next();
return res.redirect('/@' + user.username);
});
});
});

We’re using the authenticated middleware here, which comes after the route string. All this does is run that function every time the route is accessed. Nothing fancy. In the GET request, we just want to render the user. In the POST request, we set the user’s username to the form username content, and save it. If there’s an error, return next(), and if there’s no error, redirect to the new username’s page. Cool.

Let’s make the home page a bit different for the user. We’ll do this in two parts.

First, let’s update layout.pug to display the user’s username, settings, and logout , but only if the user is logged in. If not, we’ll show login and register as usual. Pug allows us to do this with basic if/else statements.

// views/layout.pug
doctype
html
head
title twttr
meta(charset="utf-8")
link(rel="stylesheet", href="/css/style.css")
body
h2
a(href="/") twttr
if user
a(href="/@" + user.username) #{user.username}
a(href="/settings") Settings
a(href="/logout") Logout
else
a(href="/login") Login
a(href="/register") Register
hr
block body

The next step is to supply that user if authenticated. Let’s hop back to index.js.

// index.js
app.get('/', (req, res) => {
if (req.session._id) {
User.findById(req.session._id, (err, user) => {
res.render('index', {user: user});
});
} else {
res.render('index');
}
});

All we’re doing here is using the findById method that all mongoose models have to get the user by the session’s ID. If it exists, we’ll render the index page with the user’s object– if not, we’ll render it without. The pug file will do the rest for us.

whats up boiiiiiiiiii

Now, if we check the main page while logged in, we should see the username, settings, and logout, instead of login/register buttons. Sweet.

Real quick– let’s fix the CSS a bit since it’s bugging me. Make a folder in public/ called css/ and inside that make a file called style.css.

// public/css/style.css
body {
font-family: "Helvetica Neue", sans-serif;
margin: 10px;
}
a { padding: 10px; }

There, at least now it’s tolerable. Next, we’ll make the page to tweet. This is going to be very similar to registering users, only we’ll be using the Tweet model instead. Create views/post.pug, and let’s jump into it.

// views/post.pug
extends layout
block body
form(target="/tweet", method="post")
textarea(placeholder="Tweet Something!", name="tweet")
input(type="submit")

Same old, same old. You should be familiar with this by now. Let’s create the route in index.js.

// index.js
app.get('/post', authenticated, (req, res) => {
res.render('post');
});

Now let’s get to the POST method for it.

// index.js
app.post('/post', authenticated, (req, res) => {
var session = req.session;
User.findById(session._id, (error, user) => {
var tweet = new Tweet({
content: req.body.tweet,
author: user._id
});
tweet.save((err, tweet) => {
if (err) throw err;
   return res.redirect('/tweet/' + tweet._id);
});
});
});

This gets the current user’s ID (given that the user is authenticated), creates a new tweet with the content as the form’s data and the author as the ID belonging to the authenticated user, then saves it, throwing an error if one is present, and if not, redirecting to tweet/:id. Phew, that was a mouthful. Let’s work on the page for the tweet display now.

Create views/tweet.pug. This will be what’s rendered when we view tweet/:id.

// views/tweet.pug
extends layout
block body
h2= user.username
p= tweet.content

By the way, the = just means “evaluate the following variable. These both do the same thing:

h2= user.username //shorter!
h2 #{user.username} //not as concise

Let’s get to work on that route.

// index.js
app.get('/tweet/:id', (req, res) => {
Tweet.findById(req.params.id, (err, tweet) => {
if (err) throw err;
  User.findById(tweet.author, (e, user) => {
if (e) throw e;
   res.render('tweet', {tweet: tweet, user: user});
});
});
});

This takes a URL parameter of id and finds the tweet with the specified ID, rendering tweet.pug with the tweet associated with the ID. Also, we’re grabbing the tweet’s author so we can show their username, etc, on the tweet’s page.

Also, we should add the /post link to the main “navigation” spot. Let’s just add this in layout.pug really quickly.

// views/layout.pug
doctype
html
head
title twttr
meta(charset="utf-8")
link(rel="stylesheet", href="/css/style.css")
body
h2
a(href="/") twttr
if user
a(href="/@" + user.username) #{user.username}
a(href="/post") Post
a(href="/settings") Settings
a(href="/logout") Logout
else
a(href="/login") Login
a(href="/register") Register
hr
block body

We should be able to post and view a tweet now!

a darn!! tweet

For the social aspect of Twitter, I’ll leave that for another time. I’ll likely do a part two for this, since this is getting to be a big read already.

Thanks for reading through all of this! If you have any questions feel free to tweet me.