DEV Community 👩‍💻👨‍💻

Jay Koontz
Jay Koontz

Posted on • Updated on

Learn how web apps work by building one in lightning speed

There’s a lot to be gained by deeply studying javascript syntax, how HTML & CSS works, etc, but at the end of the day--we’re here to build. You might not need as much JavaScript as you think to hit the ground running. In fact, you can learn as you go along, just like developers do in the real world.

We’re going to go all-out here and build a simple non-realtime chat app in native JavaScript with a REST API for the backend using Express.js. We’ll even build a quick database for it in PostgreSQL. By the end of this, you’ll see how everything comes together. You might not understand it perfectly, but that’s okay. Instead of getting stuck in tutorial hell, you’re going to use what you don’t understand to fuel your studies.

We’re not here to build a beautiful UI, or even a beautiful codebase. Just a simple UI and a quick backend to show off the concepts.

At the very end, I’ll tie in what you’ve done to how webapp development works in the real world.

I recommend using VS Code to browse and edit the codebase.

Warning: you’re about to be thrown in the deep end.

Don’t give up! In fact, move on if you’ve hit too big of a wall. But also, if you haven’t even touched HTML or JavaScript yet, check out The Odin Project’s foundations course.

How quickly can we build a server?

Very. Building the foundation for a server is usually done once, so we have some tools that can generate a good one for us. My personal favorite is provided by the Express team itself: https://expressjs.com/en/starter/generator.html.

Create a folder for your project, open your terminal or command line to it, and run the following:

npx express-generator --no-view
Enter fullscreen mode Exit fullscreen mode

Type in y if prompted to install.

Then, run npm i to install the packages that allow the server to run.

The terminal will tell you the command to run the app. Copy/paste it to run the server.
Should look something like this: DEBUG=your-project:* npm start

That’s it. Does it say Listening on port 3000? Whatever port it’s listening on, visit your browser at localhost:3000 (or your specific port).

Do you see the page? Welcome to Express!

But what’s going on inside? Where did that page come from?

Check your app.js file in VS Code. There’s a line that looks like this, go ahead and find it:

app.use(express.static(path.join(__dirname, 'public')));
Enter fullscreen mode Exit fullscreen mode

This makes Express serve the /public folder in your codebase. You could’ve named it anything so long as it matched with a real directory in your codebase.

/public contains the HTML, CSS, and (soon!) the JavaScript for your app.

Go ahead and check out /public/index.html. It’s fairly straightforward:

<html>
    <head>
         <title>Express</title>
         <link rel="stylesheet" href="/stylesheets/style.css">
    </head>
    <body>
         <h1>Express</h1>
         <p>Welcome to Express</p>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

That’s where the page came from. This index.html file is the basis of your UI. You can change it into whatever you want.

Let’s turn it into a chat app!

Building a form that submits chats to the server

Keep it simple--we’re going fast here! We’ll use id’s so JS has something to work with:

<form id="chatbox">
   <label>Message
       <input type="text" id="message"/>
   </label>
   <button type="submit">Send</button>
</form>
Enter fullscreen mode Exit fullscreen mode

So how can JS work with it? Create an index.js file in the /javascripts folder and put the following code in it--annotated in case you need to dive more deeply into the syntax:

function setEventListeners() {
 document
   // querySelector uses CSS selectors to get elements. # is for ID's
   .querySelector("#chatbox")
   // #chatbox is a form, we listen to its "submit" event here
   // Google "addEventListener js" if you'd like to learn more
   .addEventListener("submit", function (event) {
     event.preventDefault(); // keeps the page from refreshing

     // "value" is a property all inputs have in a form. for "text" inputs, it's the text
     const message = document.querySelector("#message").value;

     // learn about fetch() here: https://javascript.info/fetch
     fetch("/chats", {  // we'll have to create a /chats route in the server
       headers: new Headers({'content-type': 'application/json'}), // important!! we want to send things as JSON
       method: "post", // Google 'HTTP verbs' for more, you'll see it in the server
       body: JSON.stringify({ message }), // turns the JSON into a string for the server to parse
     })
       // fetch creates a promise. We chain .then after it for when the fetch is finished
       // Google "promises js" to learn more
       .then(function () {
         // clear it after using that same value property!
         document.querySelector("#message").value = "";
       });
   });
}

// the HTML needs to load before we can grab any element by ID!
// this will call the setEventListeners function above when DOMContentLoaded occurs
document.addEventListener("DOMContentLoaded", setEventListeners);
Enter fullscreen mode Exit fullscreen mode

If anything in that JS file doesn’t make sense after reading the comments, Google it or use javascript.info to learn more.

We can’t forget to include this script in our index.html file.
Here’s how your <head> section in index.html should look for this to work:

<head>
    <title>Express</title>
    <link rel="stylesheet" href="/stylesheets/style.css">
    <script src="javascripts/index.js"></script>
</head>
Enter fullscreen mode Exit fullscreen mode

What do we do with the server?

We need a way to receive POST requests at the /chats route to match with our fetch call. The body will have a JSON object { message: ‘this is the chat’ }, so we need to take that message and store it. Ideally, we want to do this inside 15 minutes. We’re just storing a message! Nothing fancy at all.

Real quick--what’s a route?

Routes handle the GETs, POSTs, and basically any incoming communication to the server.
Take a look at that /routes folder. We’ve been given index.js and users.js, but if we check out the route in users.js...it’s not that different from index.js:

/routes/users.js

/* GET users listing. */
router.get('/', function(req, res, next) {
 res.send('respond with a resource');
});
Enter fullscreen mode Exit fullscreen mode

/routes/index.js

/* GET home page. */
router.get('/', function(req, res, next) {
 res.render('index', { title: 'Express' });
});
Enter fullscreen mode Exit fullscreen mode

Ignore the res.render vs res.send. We see router.get(‘/’ …) for both of them. Wouldn’t they route to the same thing? Shouldn’t the Users route at least say ‘/users’?

Check out how these routes are actually hooked up to the server in /app.js:

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

.
.
.

app.use('/', indexRouter);
app.use('/users', usersRouter);
Enter fullscreen mode Exit fullscreen mode

There we go. They’re imported using require and then app.use sets the root path for the route. usersRouter gets ‘/users’, and so any fetch made to the ‘/users’ path goes through it.

Our server needs a ‘/chats’ route, so let’s set that up.

Route setup

First, we need a /routes/chats.js route file. Add that under the routes folder and add this to it:

var express = require("express");
var router = express.Router();

router.post("/", function (req, res, next) {
 // destructuring used here, it's the same as req.body.message
 // Google the concept, it's convenient!
 const { message } = req.body;
 console.log(message);
 res.sendStatus(200);
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Router.post? Yep. I’ll explain POST vs GET in more depth soon. In the meantime, notice how our fetch call in the index.js file used a “post” method. These must match for the server to know what to do.

Once the route is reached, we grab the message from the request body using destructuring (check out javascript.info for more on destructuring).

res.sendStatus(200) will send an HTTP 200 status code back to our original fetch request once it’s complete. That means everything turned out fine. You may have heard of the “404” error by now--it means a resource wasn’t found. There are other errors like 500--which means the server is broken. Status messages help the client know whether a request failed, worked just fine, didn’t exist, and more.

But wait, the route isn’t ready to use yet!

Let's hook up this route first. In app.js, import this file the same way we imported the other routes. I’ll add it to that area so you see what I mean:

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var chatsRouter = require('./routes/chats');
.
.
.
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/chats', chatsRouter);
Enter fullscreen mode Exit fullscreen mode

Now we can use that form and see that the route works. Refresh your server (turn it off and then on again with ctrl+c and then the start command!) and send a message.

You should see this on the terminal:

[the chat message you sent]
POST /chats 200 3.424 ms - 2
Enter fullscreen mode Exit fullscreen mode

See that 200? Same status that we sent. If the message shows up too, you’re all set. Great work. We’ll send something back to the client once we’ve stored data in our database.

So how do we store this message?

Setting up a PostgreSQL database isn’t that much work, and it’s one of those things that takes no prior knowledge to do. The hardest part is usually trying to remember your admin account’s username and password, so write it down somewhere--it’s just for personal use anyways.

A little bit of context to help:
The server connects to the database. It doesn’t run the database itself. We’re setting up something that runs entirely outside of Express.

Install PostgreSQL here: https://www.postgresql.org/download/

It will come with a program called pgAdmin, which provides a UI to manage your databases. It’s very handy, so open it up once it’s all ready.

You’ll need to create a database using the UI. Here’s a screenshot of what to click on:
Creating a database in pgAdmin

Give the database a useful name like “chat-app” and hit save:
Give the database a name
And boom--your first database. Not so bad, right?

Now let’s hook it up to our Express app.

Sequelize and storing our database password somewhere safe

First off, Express needs credentials to connect to the database. We don’t want those to go public, so we put them in their own file and ensure that file never leaves our computer.

At the root of your server folder, add a .env file. That’s right, just the extension .env. It’s short for environment, and it’s where we set our environment variables. Database credentials are perfect for this.

Add the following lines to the .env file:

DATABASE=chat-app
DATABASE_USER=[your username, probably postgres]
DATABASE_PASSWORD=[the password you used for the admin account]
Enter fullscreen mode Exit fullscreen mode

Now we need to install a few packages for Express to work with our .env file and our database.

Connect to Postgres with Sequelize

We need to install a few packages that help us use our .env file and connect to the database. Run npm i --save dotenv sequelize pg

Once installed, we’re ready to hook up the database to our Express app. This part isn’t hard from a syntax perspective--it’s just hard to know why we’re doing it and where to put things.

We have this library called Sequelize that acts as a middleman between Express and our Postgres database. With it, we can connect to the database, create tables, insert and edit data, etc., all with JavaScript syntax.

I’ll walk you through the setup.

At the root of your project, create a /models folder and add a db.js file to it. This file will be used to initialize a Sequelize object that can connect to our database. Here’s the contents:

var Sequelize = require('sequelize')

const sequelize = new Sequelize(
   process.env.DATABASE,
   process.env.DATABASE_USER,
   process.env.DATABASE_PASSWORD,
   {
       dialect: 'postgres',
   }
)

module.exports = { sequelize, Sequelize }
Enter fullscreen mode Exit fullscreen mode

Now for some fancy configuration. Go to your /bin/www file. It’s a weird one, but we need to instantiate our env variables and connect to the database here.

Add the dotenv and db import lines here above the app declaration:

/**
* Module dependencies.
*/
var dotenv = require('dotenv')
dotenv.config()

var db = require('../models/db')
var app = require('../app');
Enter fullscreen mode Exit fullscreen mode

In that same file, we need to use our db import to sync the database before starting our server!
So find the lines in /bin/www that look like this:

/**
* Listen on provided port, on all network interfaces.
*/

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

And wrap them like so:
db.sequelize.sync().then(() => {
 server.listen(port);
 server.on("error", onError);
 server.on("listening", onListening);
});
Enter fullscreen mode Exit fullscreen mode

Okay, can we store chats after all the weird configuration?
Not yet! We actually need a chat model for Sequelize to use. The good news is that we’re basically done with DB setup! And, this model will be super simple. Let’s code it and hook it up to our Sequelize instance.

Creating a Sequelize model

Add a file called ‘chat-model.js’ to the /models folder.
We’re going to define our model in it with just a message:

exports.ChatModel = (sequelize, DataTypes) => {
   const Chat = sequelize.define(
       "chat",
       {
           message: {
               type: DataTypes.STRING,
               field: "message"
           }
       }
   )

   return Chat
}
Enter fullscreen mode Exit fullscreen mode

I highly recommend reading the docs for Sequelize to familiarize yourself with this code: https://sequelize.org/v5/manual/getting-started.html. Don’t worry if it doesn’t all make sense at first. It may take a few readings for the concepts to sink in.

Hooking up our Chat model to our Database

We need to go back to our db.js file, import our Chat model, instantiate it, and export it.

In the end, db.js will look like this:

var Sequelize = require('sequelize')
var ChatModel = require('./chat-model').ChatModel

const sequelize = new Sequelize(
   process.env.DATABASE,
   process.env.DATABASE_USER,
   process.env.DATABASE_PASSWORD,
   {
       dialect: 'postgres',
   }
)

const Chat = ChatModel(sequelize, Sequelize)

module.exports = { sequelize, Sequelize, Chat }
Enter fullscreen mode Exit fullscreen mode

Let’s save those chat messages!

Go right back to your /routes/chats.js file. We’re going to use this Chat export to save our messages.

First, we need to import it with require. Add this line at the top:

var Chat = require('../models/db').Chat
Enter fullscreen mode Exit fullscreen mode

We’re going to add a line that uses Chat to create that message as a row in our chat database:

router.post("/", function (req, res, next) {
 // destructuring used here, it's the same as req.body.message
 // Google the concept, it's convenient!
 const { message } = req.body;
 Chat.create({
     message
 })
 res.sendStatus(200);
});
Enter fullscreen mode Exit fullscreen mode

See Chat.create()? That’s all it takes! Make sure it’s added as an object. The syntax sugar .create({ message }) is the same as writing .create({ message: message }). See this article from javascript.info to learn more.

Ready for the magic?

Alright, reset your server. If you did the setup right, there may have been some extra logs on your terminal during the starting phase. If there was an error instead, read it--it’s very likely related to one of the steps above.

If all is well, send a message through your UI! It will save in the newly-created database table.

You can check by opening pgAdmin, clicking into chat-app, clicking into Schemas -> public -> Tables, and right clicking on the ‘chats’ table. Select View/Edit Data -> All Rows:
Get all rows in pgAdmin

Did you see your message come up? Incredible, right? Sequelize also added an id, createdAt and updatedAt column for you and populated it on its own. Very convenient.

Finally, get and display all the chats

This last step makes use of everything we’ve learned so far. We’re going to grab all the chats from the database and display them in our chat app both on load and after everytime we send a chat. You might have noticed that this is not sufficient for a real-time chat app, and you’re right. It’s not. But websockets are super advanced, so we’ll just have to refresh the page or send a message to see any new ones.

Create a route that grabs all the messages

Sequelize has some handy query methods to make this easy for us. Read more about them here if you’d like: https://sequelize.org/v5/manual/getting-started.html#querying
We’re going to use Chat.findAll() for our needs.

Add this route to /routes/chats.js:

router.get('/', async function(req,res,next) {
   const chats = await Chat.findAll()
   res.json({messages: chats})
})
Enter fullscreen mode Exit fullscreen mode

Note the async/await syntax! It’s clean and allows Chat.findAll() to finish before program execution continues. Learn more about it here: https://javascript.info/async-await

Get the data and display it in the UI

We’re here. One last hard part. We need to both call this route from the UI using fetch and dynamically render the results. ..how do we do that?

All in the JavaScript! Open the /public/javascripts/index.js file. We’re going to create a couple functions to make this happen for us.

The first one will use fetch to grab the data from the server.
The next will render the messages.

The syntax is a little funky for grabbing the messages, check it out:

async function getMessages() {
   const fetchResponse = await fetch('/chats', { method: 'get' })
   const response = await fetchResponse.json()
   return response.messages
}
Enter fullscreen mode Exit fullscreen mode

It’s asynchronous, it uses fetch, and looks clean. The hardest part is remembering to call response.json() like we do above, followed by grabbing what we want off the response. You may think the response is just the messages, but it isn’t! There’s extra data in a response, check it out by using console.log on the response.

Alright, so what’s the function to render it?

Well first, let’s add a <ul> to our /public/index.html file with an ID so we can access it easily:

 <ul id="messages"></ul>
Enter fullscreen mode Exit fullscreen mode

I put it right below the form. Put it wherever you like.

Now let’s return to index.js and create a function that renders the messages in that <ul> element:

function renderMessages(messages) {
   const root = document.querySelector("#messages")

   const listItems = messages.map(messageObject => {
       return `<li>${messageObject.message}</li>`
   }).join('')

   root.innerHTML=listItems
}
Enter fullscreen mode Exit fullscreen mode

We used map to loop through all the messages and template literals to cleanly insert the message into the <li> tag. Map returns an array, so we use .join(‘’) to create a single string from the array. Then, we set the innerHTML of the <ul> to the string we created.

If that sounded Greek to you, I did that on purpose so you would read up on all these topics here:

Using these functions on init and message send

We’ve got all we need, but now we need to do more on initialization than just set some event listeners. For a project of this size, we can store all the necessary steps in an init() function like so:

async function init() {
   setEventListeners()
   const messages = await getMessages()
   renderMessages(messages)
}
// the HTML needs to load before we can grab any element by ID!
// this will call the setEventListeners function above when DOMContentLoaded occurs
document.addEventListener("DOMContentLoaded", init);
Enter fullscreen mode Exit fullscreen mode

Be sure to update the DOMContentLoaded listener to call init instead!

So what should we do on message send? We actually want to grab all the messages from the server and display them. That might sound inefficient, but for this app, we’ll be completely fine.

Remember where we cleared the value of the message input? Turn that function async by adding async before function() and then add the same await getMessages() and renderMessages lines here:

.then(async function () {
  // clear it after using that same value property!
  document.querySelector("#message").value = "";

  const messages = await getMessages();
  renderMessages(messages);
});
Enter fullscreen mode Exit fullscreen mode

The final index.js source

I know it’s hard to follow along and find out where to put all that code. Here’s my final output:

function setEventListeners() {
 document
   // querySelector uses CSS selectors to get elements. # is for ID's
   .querySelector("#chatbox")
   // #chatbox is a form, which has the "submit" listener for when that button is pressed
   // Google "addEventListener js" if you'd like to learn more
   .addEventListener("submit", function (event) {
     event.preventDefault(); // keeps the page from refreshing!

     // "value" is a property all inputs have in a form. for "text" inputs, it's the text
     const message = document.querySelector("#message").value;

     // fetch is a nice upgrade from XMLHTTPRequest. but..what's this /chat route?
     fetch("/chats", {
       // we'll have to create the /chats route before our server knows what to do with this
       method: "post", // Google this!
       headers: new Headers({ "content-type": "application/json" }), // important!! we want to send things as JSON
       body: JSON.stringify({ message }), // turns the JSON into a string for the server to parse
     })
       // fetch creates a promise. We chain .then after it for when the fetch is finished
       // Google "promises js" to learn more
       .then(async function () {
         // clear it after using that same value property!
         document.querySelector("#message").value = "";

         const messages = await getMessages();
         renderMessages(messages);
       });
   });
}

async function getMessages() {
 const fetchResponse = await fetch("/chats", { method: "get" });
 const response = await fetchResponse.json();
 return response.messages;
}

function renderMessages(messages) {
 const root = document.querySelector("#messages");

 const listItems = messages
   .map((message) => {
     return `<li>${message.message}</li>`;
   })
   .join("");

 root.innerHTML = listItems;
}

async function init() {
 setEventListeners();
 const messages = await getMessages();
 renderMessages(messages);
}
// the HTML needs to load before we can grab any element by ID!
// this will call the setEventListeners function above when DOMContentLoaded occurs
document.addEventListener("DOMContentLoaded", init);
Enter fullscreen mode Exit fullscreen mode

You did it!!

That’s a full client/server web app you’ve created! Is the UI pretty? No. Is it the best coding architecture? It’s not. But it works, and you can build off what you’ve learned here to create bigger projects.

...wait, what did I do?

Now that you’ve set up a client and server, let’s fill in the holes.

  • How client/server architectures work
  • How HTTP lets use communicate between our UI and our server
  • How Databases work
  • “I felt so lost reading this” and why that’s not because you’re dumb
  • What’s next?

How client/server architectures work & different ways of hosting HTML

The relationship we set up looks like this:
[client <-> server <-> database]
It’s not the only way to do things. We created a Single Page App.

Another way to get around it is to not have the server serve the static assets at all and use something else to serve your HTML.

The server allowed for index.html to be accessed at the root level, localhost:3000.

How HTTP lets use communicate between our UI and our server

It’s in the name: Hyper Text Transfer Protocol. HTTP is just text. It’s used to hold so many things, but it’s all text. Servers and browsers standardized what this text says to make it useful for communicating to each other.

Open up Chrome’s Dev Tools (cmd + option + i for Mac and Shift + CTRL + J for Windows)
Check out the Network tab and refresh the page. You’ll see all the requests made. Clicking into these requests show a whole world of information: header info, payloads & their sizes, server paths, and even diagnostic info like how long a request took.

If you visit localhost:3000/chats, you’ll actually see the JSON response for all the chats you can grab when doing a GET call to /chats. So what’s GET? It’s an HTTP verb.

The browser sends a GET request to that URL every time you use it to visit or refresh a page. We sent one directly with fetch, and the same server route is reached when a browser visits a URL. Browsers send GET requests to any URL you want, it’s how the web works.

There are other HTTP verbs we can use:
POST, generally used to “create” resources on the server
PUT, generally used to update a resource by providing the entire object to replace it with
PATCH, generally used to update a resource by providing the parts of the resource to update
DELETE, generally used to delete resources

GET requests have one major difference with POST aside from its responsibility:
With POST requests, you generally provide the resource in the body as an object, like we did above.
With GET requests, if you need to provide extra variables, you do so in the URL itself. You may have seen URLs like “https://localhost:3000/chats?key=val

These resources dive deeper into this subject:

How databases work

The sheer number of databases you could use would make your head spin. There isn’t a single answer to which database to use for your app, and there are more options every year.

The classics you’ll hear the most about at the beginner level are PostgreSQL, MySQL, and MongoDB. Stick to these and you’ll be fine.

Postgres and MySQL are relational databases which use SQL to interact with, add to, and edit your stored data. MongoDB falls under “NoSQL” and has its own syntax for database interaction.

No matter how much flack any one database gets, they’ve all been used to start and run highly successful companies. MongoDB can be a bit quicker to get started with, but you get a bit more respect from the developer community for understanding SQL, the language used to interact with Postgres, MySQL, and other relational databases. Learn more about these topics here:
Youtube, etc

“I felt so lost reading this” and why that’s not because you’re dumb

You’re looking at how much you know right now and feel like you’re coming up short. Lots of people do this. Everyone’s confidence, absolutely everyone’s, is humbled by how difficult this field is. Here’s the thing: if you judge yourself on how much you know, you’ll always fall short. Instead, judge yourself on being able to figure things out.

The unknowns never end. After 7 years in this field, I use Google constantly. Everyone does. It’s the real skill to learn.

See a topic you want more info on? YouTube it. Google around. Find a relevant online course through Udemy. StackOverflow likely has answers.

This project is meant to throw you in the deep end. I bet if you did it a 2nd time, it’d go a bit smoother. Do it a 3rd time, and you could probably add to it with ease.

Learning never stops in this profession. Ever. The beginning of any project is typically research-oriented. I had to have a small refresher myself to make sure I was using fetch correctly. The concepts will come up again and again, but the concept that reigns supreme is how to find things out with the right Google search query.

What’s next?

It’s time to dive deeper into each of these topics: building a UI, building a server, modeling data for your database. Your knowledge of syntax will solidify along the way.

Top comments (0)

Take a look at this:

Settings

Go to your customization settings to nudge your home feed to show content more relevant to your developer experience level. 🛠