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
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;
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);
});
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;
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);
...
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);
});
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;
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);
...
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);
})
}
...
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": "..."
}
]
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);
})
}
That's everything! Now there are full-CRUD capabilities.
Top comments (0)