DEV Community

Cover image for Express Kickoff
Konstantin Stanmeyer
Konstantin Stanmeyer

Posted on

Express Kickoff

The switch to a JavaScript-only ecosystem has been extremely weird, but satisfying to see things I could do in ActiveRecord and ActiveStorage with Rails are possible, and sometimes easier with Express. The steps to perform SQL queries feel natural and simple, and with Sequelize doing almost all the work for you. If you are new to using JavaScript as a backend, this blog will introduce you to a basic API with full-CRUD, and show just how easy and familiar this process can be.

Here's my beginner experience building a basic application using Node/Express.js with Sequelize/MySQL to connect with the database. This is about the API-only!:

The packages below were used for this example

npm install body-parser express mysql2 nodemon sequelize
Enter fullscreen mode Exit fullscreen mode

Create an app.js file in the root of the project.

Before defining any parts of the actual application within app.js, I needed to set up my Sequelize database. I'll skip this setup since it uses a third-party application(MySQL Workbench), but basically it creates a database I can find within my Node application, then connect to and query. To establish this connection, I created a util folder (holds helper functions) with a database.js file to hold this code. The file specifies key pieces of information to securely connect, including user info, the database's name, and the database type, which in my case is MySQL. The final file looks like so:

// ./util/database.js
const Sequelize = require('sequelize'); // using the sequelize package

const sequelize = new Sequelize('mysql-database', 'root', 'password...', {
  dialect: 'mysql',
  host: 'localhost'
});

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

App.js will be where the basic application is defined, and also for implementing code to sync any latter changes you will make to the schema. Alongside all the necessary imports here is what the basic app.js file looks like:

// ./app.js
const express = require('express');
const bodyParser = require('body-parser');

const sequelize = require('./util/database');

const app = express();

app.use(bodyParser.json());

sequelize.sync()
.then(res => {
  app.listen(3000); // PORT 3000
})
.catch(err => {
  console.log(err);
});
Enter fullscreen mode Exit fullscreen mode

BodyParser will come in handy later for post requests, otherwise we would receive undefined in the body of requests.

Then, the models may be defined. Like Rails, we can define a schema, this time within the model, not migrations. Changes will be noticed and applied when the server is restarted, via the sequelize.sync() method. Within a models folder, define a Product model like so:

// ./models/product.js
const Sequelize = require('sequelize');

const sequelize = require('../util/database');

const Product = sequelize.define('product', {
    id: {
        type: Sequelize.INTEGER,
        autoIncrement: true,
        allowNull: false,
        primaryKey: true
    },
    name: {
        type: Sequelize.STRING,
        allowNull: false,
    },
    price: {
        type: Sequelize.DOUBLE,
        allowNull: false,
    },
    imageUrl: {
        type: Sequelize.STRING,
        allowNull: false
    }
});

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

This is where I think JavaScript starts to show a couple fallbacks compared to Rails (for this use-case), especially their syntax, and boiler-plate code. It feels like I'm going around hurdles to use a backend in a multi-purpose language, which makes sense and gives more possibility in the contrary, but maybe this is just me being spoiled with ActiveRecord while using Rails; Rails feels like it was built to make queries.

Since I wanted to be exposed to SQL relationships in JavaScript before my switch to NoSQL, I decided to create a second model used to add simple comments to products. The model define any relationships just two attributes: Id (which is also auto-applied), and description. After restarting the server, we can see the model's schema has synced within the database, but still without the foreign key we want, how do we define that? In app.js, like so:

// ./app.js
...
Comment.belongsTo(Product, { constraints: true, onDelete: 'CASCADE'});
Product.hasMany(Comment);
...
Enter fullscreen mode Exit fullscreen mode

This also adds a dependency on comment instances, which will now automatically be also deleted if the product they're attached to gets deleted.

A cool feature we also get with relationships in Sequelize, in this case, we get methods like Product.createComment(), which will automatically assign a foreign key on the table, and create a comment instance.

However, after these changes, if the table already exists, they won't be noticed and we must drop the tables to force them. This change must only run once, too. To do so, change the .sync() method called in app.js like this:

// ./app.js
...
sequelize.sync({ force: true })
.then(res => {
    app.listen(4000);
})
.catch(err => {
    console.log(err);
});
Enter fullscreen mode Exit fullscreen mode

Here, run npm run dev, start the server once, then revert that line of code to not contain { force: true }. Now the changes exist on the database and the comments have a product_id foreign key.

Next, create a routes folder within the root of your directory and add a product.js and comment.js file. Within these files is where we define the controllers, and which requests fire which ones. This felt almost identical to Rails where you'd also just define which route (params and some other features also work the same), and which method gets called in response, but with slightly more boiler-plate code as you can see:

// ./routes/product.js

const express = require('express');

const productsController = require('../controllers/products');

const router = express.Router();

router.get('/product/:id', productsController.getProduct);

router.get('/products', productsController.getProducts);

router.post('/product', productsController.addProduct);

router.post('/edit-product/:id', productsController.editProduct);

router.delete('/delete-product/:id', productsController.deleteProduct);

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

The code to set up for comments was done in the same way, but with only a get request for all comments for a specific post, and a request to post a comment to a specific post.

To implement these routes, we must add middleware in app.js:

// ./app.js
...
const productRoutes = require('./routes/product');

app.use(productRoutes);
...
Enter fullscreen mode Exit fullscreen mode

Next is the actual controllers, which will be within a controllers folder, and named products.js and comments.js. Controllers work somewhat similarly to Rails, where there will be a method/function defined, usually taking in a request, then sending a response. The hardest part is just understanding promises and how Sequelize uses them. Everything done under the hood makes data-interaction very easy. Here is a function to grab all instances of Products:

// ./controllers/products.js
...
exports.getProducts = (req,res) => {
    Product.findAll()
    .then(prods => {
        res.json(prods);
    })
    .catch(err => {
        console.log(err);
    })
}
...
Enter fullscreen mode Exit fullscreen mode

Here is what's happening. When a user requests '/products' as a get request, getProducts runs, then creating a fetch request, which is then resolved or caught if an error is thrown. If it is successful the products are sent in a response, as json.

This fetch would return something like:

[
    {
        "id": 1,
        "name": "Toothbrush",
        "price": 3,
        "imageUrl": "N/A",
        "createdAt": "...",
        "updatedAt": "..."
    },
    {
        "id": 2,
        "name": "Socks",
        "price": 6,
        "imageUrl": "N/A",
        "createdAt": "...",
        "updatedAt": "..."
    },
    {
        "id": 3,
        "name": "Napkins",
        "price": 4,
        "imageUrl": "N/A",
        "createdAt": "...",
        "updatedAt": "..."
    }
]
Enter fullscreen mode Exit fullscreen mode

Sequelize makes these queries simple and sometimes just typing something you might think will work, may actually work. Here is the entire products controller to show full-CRUD and just how easy it is:

// ./controllers/products.js
const Product = require('../models/product');

exports.addProduct = (req,res) => {
    Product.create({
        name: req.body.name,
        price: req.body.price,
        imageUrl: req.body.imageUrl
    })
    .then(response => {
       res.json(response);
    })
    .catch(err => {
        console.log(err);
    })
}

exports.getProduct = (req,res) => {
    Product.findByPk(req.params.id)
    .then(prod => {
        res.json(prod.dataValues);
    })
    .catch(err => {
        console.log(err);
    });
}

exports.getProducts = (req,res) => {
    Product.findAll()
    .then(prods => {
        res.json(prods);
    })
    .catch(err => {
        console.log(err);
    })
}

exports.editProduct = (req, res) => {
    Product.findByPk(req.params.id)
    .then(product => {
        const updatedName = req.body.name;
        const updatedImage = req.body.imageUrl;
        const updatedPrice = req.body.price;
        product.name = updatedName;
        product.imageUrl = updatedImage;
        product.price = updatedPrice;
        return product.save();
    })
    .then(response => {
        res.json(response);
    })
    .catch(err => {
        console.log(err);
    })
}

exports.deleteProduct = (req,res) => {
    Product.findByPk(req.params.id)
    .then(prod => {
        return prod.destroy();
    })
    .then(res => {
        console.log(res);
    })
    .catch(err => {
        console.log(err);
    })
}
Enter fullscreen mode Exit fullscreen mode

That's everything! Now there are full-CRUD capabilities.

Latest comments (0)