Build a Blog Using Express.js and React in 30 Minutes

Publikováno: 14.1.2019

Building a web server sounds complicated, but it doesn't have to be! What if I told you that you can create a web server with just a couple lines of code? Yes! You can do things like this using Exp...

Celý článek

Building a web server sounds complicated, but it doesn't have to be! What if I told you that you can create a web server with just a couple lines of code? Yes! You can do things like this using Express.js (the most popular web framework for Node developers).

In this tutorial you'll learn how to create a simple "My Blog" App that allows a user can create, edit, and delete a post. You'll also learn how to add authentication to the app so users have to sign in before being allowed to do any CRUD (create, read, update, delete) actions. The tutorial will use the fabulous Okta NodeJS OIDC Middleware for authentication. The back-end will use Express.js to power the server, Sequelize for storing and manipulating data, and Epilogue for automatically generating REST endpoints and controllers from the Sequelize data models.

Express.js App Prerequisites

Before you begin, head over to https://developer.okta.com/ and create an account, or log in if you’ve already signed up. The Okta service is free, and is what you'll be using to handle the user registration, login, logout, etc. It's an API service for managing user identities.

Once you've signed up for Okta, follow these steps below to configure it. This will make it easier to implement user authentication later on.:

After you log in, you will see the Org Url in the top right corner of your dashboard, save it somewhere for later use. Click Application on the navigation menu.

  • Click the Add Application button.

  • Select Web as the software, click the Next button.

  • Enter the following information, then click the Done button.

  • Name: My Blog

  • Base URIs: http://localhost:3000/

  • Login redirect URIs: https://localhost:3000/authorization-code/callback

You will then be redirected to the general page, scroll down you will see Client ID and Client Secret values, save these somewhere for later use. These are your app's API keys that it will use to securely handle user authentication later on via the OpenID Connect protocol.

NOTE: You might be wondering why you haven't written any code and are already using an API service here. The reason is that handling user authentication yourself is actually pretty tricky. There are very few ways to do it correctly, and lots of easy ways to mess up that are non-obvious. Using a free service like Okta to handle this piece of the application will make your project code a lot simpler later on.

The way the Okta service works is by using the OAuth/OpenID Connect protocols behind the scenes to handle all of your user management. Okta handles user registration, login, password reset, multi-factor authentication, and lots of other stuff.

Set Up Express.js

You should have node and npm installed, my Node version is v10.10.0 and npm version is 6.4.1. Create a project folder and basic set up with the following:

mkdir myblog
cd myblog
npm init

Continue selecting enter to accept all default settings.

You should now have this folder structure:

myblog
└── package.json

0 directories, 1 file

Now, add two new files, index.js and .env, to your folder, so your project ends up looking like this:

myblog
├── .env
├── index.js
└── package.json

0 directories, 3 files

Now you can install the required npm modules that are needed for this Express.js app. To do so, run the following commands:

npm install express@4.16.4 --save

(module needed to start the Express web application)

npm install cors@2.8.5 --save

(module enable Cross-origin resource sharing)

npm install body-parser@1.18.3 --save

(module that helps to parse incoming request bodies)

npm install dotenv@6.2.0 --save

(module that will load our .env file into process.env variables)

npm install nodemon@1.18.9 --save-dev

(tool that helps automatically restart the application when file changed)

These two npm modules are for Okta authentication.

npm install @okta/oidc-middleware@1.0.2 express-session@1.15.6 --save

In your .env file, use the Org URL, Client ID, and Client Secret you got from the Okta console to fill in the following and paste it in the file (you got them in the setup credentials steps above):

OKTA_ORG_URL={yourOktaOrgUrl}
OKTA_CLIENT_ID={yourClientId}
OKTA_CLIENT_SECRET={yourClientSecret}
REDIRECT_URL=http://localhost:3000/authorization-code/callback
RANDOM_SECRET_WORD='super secret'

NOTE: The RANDOM_SECRET_WORD setting should be a random string you type out. Just bang on the keyboard for a second to output a long random string, and use that value. This value should never be checked into source control as it is used to manage the integrity of your user sessions. It must be kept private on your web servers and should be extremely hard to guess.

In your index.js file, paste in the following code:

require('dotenv').config();
const express = require('express');
const path = require('path');
const cors = require('cors');
const bodyParser = require('body-parser');
const session = require('express-session');
const { ExpressOIDC } = require('@okta/oidc-middleware');
const app = express();
const port = 3000;

// session support is required to use ExpressOIDC
app.use(session({
    secret: process.env.RANDOM_SECRET_WORD,
    resave: true,
    saveUninitialized: false
}));

const oidc = new ExpressOIDC({
    issuer: `${process.env.OKTA_ORG_URL}/oauth2/default`,
    client_id: process.env.OKTA_CLIENT_ID,
    client_secret: process.env.OKTA_CLIENT_SECRET,
    redirect_uri: process.env.REDIRECT_URL,
    scope: 'openid profile',
    routes: {
        callback: {
            path: '/authorization-code/callback',
            defaultRedirect: '/admin'
        }
    }
});

// ExpressOIDC will attach handlers for the /login and /authorization-code/callback routes
app.use(oidc.router);
app.use(cors());
app.use(bodyParser.json());

app.get('/', (req, res) => {
  res.send('<h1>Welcome!!</h1>');
});

app.listen(port, () => console.log(`My Blog App listening on port ${port}!`))

Let me explain what the above code does. The line that starts with app.use(session…) created session middleware with the options we passed it. This is required for ExpressOIDC's configuration. OIDC stands for OpenID Connect, it is an authentication layer on top of OAuth 2.0. You can learn more about the OpenID Connect & OAuth 2.0 API here.

The line that starts with const oidc = new ExpressOIDC(...) created an instance of ExpressOIDC with the option we passed in. It enables your application to participate in the authorization code flow by redirecting the user to Okta for authentication and handling the callback from Okta. Once the flow is completed, a local session is created and the user context is saved for the duration of the session.

The app.use(oidc.router) line is required in order for ensureAuthenticated and isAuthenticated to work. It also adds the following route:

  • /login - redirects to the Okta sign-in page by default

  • /authorization-code/callback - processes the OIDC response, then attaches user info to the session

Now that work is done, you can move on to the next step. In package.json, add "start": "./node_modules/.bin/nodemon index.js" under scripts. This will allow you to simply type npm start to start the application. nodemon is a utility that will monitor for any changes in your source and automatically reload, so you don’t have to restart your server manually.

"scripts": {
    "start": "nodemon index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
 },

Run npm start then go to http://localhost:3000/ and you should see:

Now you have your Express.js app set up! Next you’ll integrate with OpenID Connect (OIDC) to let the user sign in.

Add Authentication in Express

Because we have added app.use(oidc.router) to our index.js, we got the /login route for free, which means we don't have to set it up ourselves. Anytime a request goes to /login, Okta middleware will take care for us.

Let's add a link to let user login. In index.js, replace:

app.get('/', (req, res) => {
  res.send('<h1>Welcome!!</h1>');
});

With:

app.get('/home', (req, res) => {
 res.send('<h1>Welcome!!</div><a href="/login">Login</a>');
});

app.get('/admin', (req, res) =>{
 res.send('Admin page');
});

Now, when you visit http://localhost:3000/home, you should see the login link. Be aware that we added /home after http://localhost:3000.

Click on the login link, you will see the Okta login page. If you log in successfully, you will be redirected back to the URL that you provided to OIDC middleware (the defaultRedirect URL)

const oidc = new ExpressOIDC({
 issuer: `${process.env.OKTA_ORG_URL}/oauth2/default`,
 client_id: process.env.OKTA_CLIENT_ID,
 client_secret: process.env.OKTA_CLIENT_SECRET,
 redirect_uri: process.env.REDIRECT_URL,
 scope: 'openid profile',
 routes: {
   callback: {
     path: '/callback',
     defaultRedirect: '/admin'
   }
 }
});

In our application set up, we provided /admin as the defaultRedirect URL in the ExpressOIDC setup, so now you should see the admin page.

Add oidc.ensureAuthenticated() to the /admin route, so if someone gets the link, he/she still can't see the page without logging in.

app.get('/admin', oidc.ensureAuthenticated(), (req, res) =>{
  res.send('Admin page');
})

Add the following routes after get('/admin', …), so the user can log out and when they visit other unexpected pages, we will redirect them back to home page.

app.get('/logout', (req, res) => {
  req.logout();
  res.redirect('/home');
});

app.get('/', (req, res) => {
  res.redirect('/home');
});

You've now got an app built using Express.js and Okta (for OIDC), so let's move on to the next step.

Set Up Your Database in Express.js

We are going to use SQLite as our database for storage, and Sequelize as our ORM. We are also going to use Epilogue to generate the REST endpoints. Without Epilogue, you would need to manually setup the POST, GET, PUT, and DELETE API endpoints for blog posts manually.

Install the following modules:

npm install sqlite3@4.0.4 sequelize@4.42.0 epilogue@0.7.1 --save

And then add the following code to the top of your index.js after the require section:

const Sequelize = require('sequelize');
const epilogue = require('epilogue'), ForbiddenError = epilogue.Errors.ForbiddenError;

Add the following code to the bottom of your index.js after the routes:

const database = new Sequelize({
    dialect: 'sqlite',
    storage: './db.sqlite',
    operatorsAliases: false,
});

const Post = database.define('posts', {
    title: Sequelize.STRING,
    content: Sequelize.TEXT,
});

epilogue.initialize({ app, sequelize: database });

const PostResource = epilogue.resource({
    model: Post,
    endpoints: ['/posts', '/posts/:id'],
});

PostResource.all.auth(function (req, res, context) {
    return new Promise(function (resolve, reject) {
        if (!req.isAuthenticated()) {
            res.status(401).send({ message: "Unauthorized" });
            resolve(context.stop);
        } else {
            resolve(context.continue);
        }
    })
});

database.sync().then(() => {
    oidc.on('ready', () => {
        app.listen(port, () => console.log(`My Blog App listening on port ${port}!`))
    });
});

oidc.on('error', err => {
    // An error occurred while setting up OIDC
    console.log("oidc error: ", err);
});

You have just set up the database using Sequelize and created the REST endpoints using Epilogue. I will explain more below.

The line that starts with const database = new Sequelize(...) sets up a connection with the SQLite database and tells the database to store the data in ./db.sqlite.

The line that starts with const Post = database.define('posts'...) defines the model which represents a table in the database. We are going to store the title and the context as strings in the database.

The line epilogue.initialize({ app, sequelize: database }) initializes Epilogue with our Express.js app and the database we just set up.

The line that starts with const PostResource = epilogue.resource created the REST resource, so now we have the create, list, read, update, and delete controllers with corresponding endpoints for our post.

We also added an authentication check to all CRUD routes using the code in PostResource.all.auth section so that all endpoints are protected:

PostResource.all.auth(function (req, res, context) {
    return new Promise(function (resolve, reject) {
        if (!req.isAuthenticated()) {
            res.status(401).send({ message: "Unauthorized" });
            resolve(context.stop);
        } else {
            resolve(context.continue);
        }
    })
});

By now, your index.js file should look like this:

require('dotenv').config();
const express = require('express');
const path = require('path');
const cors = require('cors');
const bodyParser = require('body-parser');
const session = require('express-session');
const { ExpressOIDC } = require('@okta/oidc-middleware');
const Sequelize = require('sequelize');
const epilogue = require('epilogue'), ForbiddenError = epilogue.Errors.ForbiddenError;
const app = express();
const port = 3000;

// session support is required to use ExpressOIDC
app.use(session({
    secret: process.env.RANDOM_SECRET_WORD,
    resave: true,
    saveUninitialized: false
}));

const oidc = new ExpressOIDC({
    issuer: `${process.env.OKTA_ORG_URL}/oauth2/default`,
    client_id: process.env.OKTA_CLIENT_ID,
    client_secret: process.env.OKTA_CLIENT_SECRET,
    redirect_uri: process.env.REDIRECT_URL,
    scope: 'openid profile',
    routes: {
        callback: {
            path: '/authorization-code/callback',
            defaultRedirect: '/admin'
        }
    }
});

// ExpressOIDC will attach handlers for the /login and /authorization-code/callback routes
app.use(oidc.router);

app.use(cors());
app.use(bodyParser.json());

app.get('/home', (req, res) => {
    res.send('<h1>Welcome!!</h1><a href="/login">Login</a>');
});

app.get('/admin', oidc.ensureAuthenticated(), (req, res) => {
    res.send('Admin page');
});

app.get('/logout', (req, res) => {
  req.logout();
  res.redirect('/home');
});

app.get('/', (req, res) => {
  res.redirect('/home');
});

const database = new Sequelize({
    dialect: 'sqlite',
    storage: './db.sqlite',
    operatorsAliases: false,
});

const Post = database.define('posts', {
    title: Sequelize.STRING,
    content: Sequelize.TEXT,
});

epilogue.initialize({ app, sequelize: database });

const PostResource = epilogue.resource({
    model: Post,
    endpoints: ['/posts', '/posts/:id'],
});

PostResource.all.auth(function (req, res, context) {
    return new Promise(function (resolve, reject) {
        if (!req.isAuthenticated()) {
            res.status(401).send({ message: "Unauthorized" });
            resolve(context.stop);
        } else {
            resolve(context.continue);
        }
    })
});

database.sync().then(() => {
    oidc.on('ready', () => {
        app.listen(port, () => console.log(`My Blog App listening on port ${port}!`))
    });
});

oidc.on('error', err => {
    // An error occurred while setting up OIDC
    console.log("oidc error: ", err);
});

The backend setup is now done, but we still need to build the UI to demonstrate it's working. Let's create the UI using React.

Create Your User Interface in Express.js with React

If you are going to build a production-ready app, you should ideally install React in your repo, but since our app is for demo purposes, we'll use React via a content delivery network (CDN) instead, to keep things simple.

Create a public folder under myblog, then create the files admin.html, admin.js, home.html, home.js in the public folder. Your folder structure should look like this:

myblog
├── .env
├── index.js
├── package.json
└── public
    ├── admin.html
    ├── admin.js
    ├── home.html
    └── home.js

1 directory, 7 files

Because you are using React, you’ll also use JavaScript to create the UI component. admin.js will contain the React code that we need for the admin page. This page will only be visible to the user after they log in. It will display all the blog posts and allow the user to create new posts, as well as update or delete existing posts. home.js will contain the welcome page and a login button. You are going to embed admin.js in admin.html and home.js in home.html.

Paste the following code in home.html. It will load the React JavaScript and bootstrap CSS files for the home page.

<!DOCTYPE html>
<html>

<head>
   <meta charset="UTF-8">
   <title>Home Page</title>
   <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
       crossorigin="anonymous">

   <script src="https://unpkg.com/react@16/umd/react.production.min.js" crossorigin></script>
   <script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js" crossorigin></script>
   <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

</head>

<body class="bg-light">
   <div class="container">
       <div id="root"></div>
   </div>
   <script src="home.js" type="text/babel"></script>

   <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
       crossorigin="anonymous"></script>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
       crossorigin="anonymous"></script>
   <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
       crossorigin="anonymous"></script>
</body>

</html>

Paste the following code in home.js. It will render the navigation menu with the Login button.

const e = React.createElement;

const AppNav = () => (
   <nav class="navbar navbar-dark bg-dark">
       <a class="navbar-brand" href="#">My Blog</a>
       <a role="button" class="btn btn-outline-info navbar-btn" href="/login">Login</a>
   </nav>
);

class Home extends React.Component {
   constructor(props) {
       super(props);
   }

   render() {
       return (
           <div>
               <AppNav />
               <div class="card mt-4" Style="width: 100%;">
                   <div class="card-body">
                       Please login to see your posts.
         </div>
               </div>
           </div>
       );
   }
}

const domContainer = document.querySelector('#root');
ReactDOM.render(e(Home), domContainer);

Paste the following code in admin.html. This will load the necessary files for React and Bootstrap in the admin page.

<!DOCTYPE html>
<html>

<head>
   <meta charset="UTF-8">
   <title>Admin Page</title>
   <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
       crossorigin="anonymous">

   <script src="https://unpkg.com/react@16/umd/react.production.min.js" crossorigin></script>
   <script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js" crossorigin></script>
   <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

</head>

<body class="bg-light">
   <div class="container">
       <div id="root"></div>
   </div>
   <script src="admin.js" type="text/babel"></script>

   <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
       crossorigin="anonymous"></script>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
       crossorigin="anonymous"></script>
   <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
       crossorigin="anonymous"></script>
</body>

</html>

Paste the following code into admin.js. It will render the navigation menu with the Logout button. It will also render the Add New Post button.

'use strict';
const e = React.createElement;

const AppNav = () => (
   <nav class="navbar navbar-dark bg-dark">
       <a class="navbar-brand" href="#">My Blog</a>
       <a role="button" class="btn btn-outline-info navbar-btn" href="/logout">Logout</a>
   </nav>
);

const Card = ({ item, handleSubmit, handleEdit, handleDelete, handleCancel }) => {
   const { title, content, editMode } = item;

   if (editMode) {
       return (
           <div class="card mt-4" Style="width: 100%;">
               <div class="card-body">
                   <form onSubmit={handleSubmit}>
                       <input type="hidden" name="id" value={item.id} />
                       <div class="input-group input-group-sm mb-3">
                           <input type="text" name="title" class="form-control" placeholder="Title" defaultValue={title} />
                       </div>
                       <div class="input-group input-group-sm mb-3">
                           <textarea name="content" class="form-control" placeholder="Content" defaultValue={content}></textarea>
                       </div>
                       <button type="button" class="btn btn-outline-secondary btn-sm" onClick={handleCancel}>Cancel</button>
                       <button type="submit" class="btn btn-info btn-sm ml-2">Save</button>
                   </form>
               </div>
           </div>
       )
   } else {
       return (
           <div class="card mt-4" Style="width: 100%;">
               <div class="card-body">
                   <h5 class="card-title">{title || "No Title"}</h5>
                   <p class="card-text">{content || "No Content"}</p>
                   <button type="button" class="btn btn-outline-danger btn-sm" onClick={handleDelete}>Delete</button>
                   <button type="submit" class="btn btn-info btn-sm ml-2" onClick={handleEdit}>Edit</button>
               </div>
           </div>
       )
   }
}

class Admin extends React.Component {
   constructor(props) {
       super(props);
       this.state = { data: [] };
   }

   componentDidMount() {
       this.getPosts();
   }

   getPosts = async () => {
       const response = await fetch('/posts');
       const data = await response.json();
       data.forEach(item => item.editMode = false);
       this.setState({ data })
   }

   addNewPost = () => {
       const data = this.state.data;
       data.unshift({
           editMode: true,
           title: "",
           content: ""
       })
       this.setState({ data })
   }

   handleCancel = async () => {
       await this.getPosts();
   }

   handleEdit = (postId) => {
       const data = this.state.data.map((item) => {
           if (item.id === postId) {
               item.editMode = true;
           }
           return item;
       });
       this.setState({ data });
   }

   handleDelete = async (postId) => {
       await fetch(`/posts/${postId}`, {
           method: 'DELETE',
           headers: {
               'content-type': 'application/json',
               accept: 'application/json',
           },
       });
       await this.getPosts();
   }

   handleSubmit = async (event) => {
       event.preventDefault();
       const data = new FormData(event.target);

       const body = JSON.stringify({
           title: data.get('title'),
           content: data.get('content'),
       });

       const headers = {
           'content-type': 'application/json',
           accept: 'application/json',
       };

       if (data.get('id')) {
           await fetch(`/posts/${data.get('id')}`, {
               method: 'PUT',
               headers,
               body,
           });
       } else {
           await fetch('/posts', {
               method: 'POST',
               headers,
               body,
           });
       }
       await this.getPosts();
   }

   render() {
       return (
           <div>
               <AppNav />
               <button type="button" class="mt-4 mb-2 btn btn-primary btn-sm float-right" onClick={this.addNewPost}>
                   Add New Post
               </button>
               {
                   this.state.data.length > 0 ? (
                       this.state.data.map(item =>
                           <Card item={item}
                               handleSubmit={this.handleSubmit}
                               handleEdit={this.handleEdit.bind(this, item.id)}
                               handleDelete={this.handleDelete.bind(this, item.id)}
                               handleCancel={this.handleCancel}
                           />)
                   ) : (
                           <div class="card mt-5 col-sm">
                               <div class="card-body">You don't have any posts. Use the "Add New Post" button to add some new posts!</div>
                           </div>
                       )
               }
           </div >
       );
   }
}

const domContainer = document.querySelector('#root');
ReactDOM.render(e(Admin), domContainer);

I have attached a handler function to each button. For example, for the Save button, I have attached the handleSubmit function. With this function, when the user clicks the Save button in a card, it checks whether the current card has an ID to tell if it’s a new post or the user is trying to update an existing post. If it’s a new post, it will make a POST API call to the /posts API with the title and the context as POST body. If it’s an existing post, it will make a PUT API call to the /posts/:id API with the updated title and context as PUT body.

NOTE: It’s important to add a handler function to each button because when the user clicks the button, some actions should be triggered, and you want to execute these actions in the handler function.

Now you need to update your index.js file to use admin.html whenever a user visits /admin and use home.html when a user visits /home.

Add this line after the line app.use(bodyParser.json()):

app.use(express.static(path.join(__dirname, 'public')));

Update the app.get('/home', ...) and app.get('/admin', ...) route to:

app.get('/home', (req, res) => {
   res.sendFile(path.join(__dirname, './public/home.html'));
});

app.get('/admin', oidc.ensureAuthenticated(), (req, res) => {
   res.sendFile(path.join(__dirname, './public/admin.html'));
});

Now, if you go to http://localhost:3000/home, run npm start and log in, you should able to add new posts, update, and delete existing posts!

Woohoo! You have now built a fully functioning single page blog app, connected it to a REST API server, and secured it with authentication via Okta’s ExpressOIDC. You can hopefully see here how easy it is to implement the authorization code flow using Okta’s oidc-middleware library.

Learn More About Express.js, Node, and React

I hope you found this post helpful. If you want to learn more about Node.js, Express.js, or React, there are many great posts on the Okta developer blog. Here are a few to get you started:

Andddddd. If you liked this post, please tweet us and let us know! We love hearing from you! =)

Nahoru
Tento web používá k poskytování služeb a analýze návštěvnosti soubory cookie. Používáním tohoto webu s tímto souhlasíte. Další informace