Learn how to use dependency injection in a JavaScript application to decouple a MySQL database from an express HTTP server. This will help us be able to mock the database for a future video.
đź”— Code: https://github.com/Sam-Meech-Ward/dependency_injection_js
There are a few benefits to using dependency injection, but the best, most useful benefit and biggest reason to use dependency injection is to make your code more testable.
In this article we’re going to take a look at what dependency injection is by using an example of an express app that stores data in a MySQL database.
The App
The following code block is a basic express app allows a username and password to be stored in a MySQL database when a POST request is made to /users
app.js
import express from 'express'
import mysql from 'mysql2'
const app = express()
var connection = mysql.createPool({
host : 'localhost',
user : 'root',
database : 'some_database'
})
app.use(express.json())
app.post('/users', async (req, res) => {
// ...
})
export default app
We’re exporting the express app from this file to make it easier to write unit tests for the API,then we can have a server.js that imports app and has it listen on port 8080:
server.js
import app from './app.js'
app.listen(8080, () => console.log("listening on port 8080"))
So running node server.js
will start the express server.
The implementation of the post request will look like this:
app.post('/users', async (req, res) => {
// 1
const { username, password } = req.body
try {
// 2
const [rows] = await connection.promise().query(
`SELECT *
FROM users
WHERE username = ?`,
[username]
)
// 3
if (rows.length > 0) {
res.status(400).send({error: "username already taken"})
return
}
// 4
const { insertId } = await connection.promise().query(
`INSERT INTO users (username, password)
VALUES (?, ?)`,
[username, password]
)
// 5
res.send({ userId: insertId })
} catch (error) {
// 6
console.log(error)
res.sendStatus(500)
return
}
})
- Grab the username and password from the post body.
- Check if the user already exists in the database.
- Send an error if the user already exists.
- Insert the username and password into the database.
- Send the userId back to the client.
- Handle any errors.
This is a very simple example that’s missing a lot of necessary things that you would need in a production application. Things like username and password validation, and password hashing. We should also store the userId in a session, or encrypted cookie, or JWT, but this example is good enough for now.
Currently all of the database specific code is mixed in with the HTTP server specific code. That means that any changes to the database code might also effect the HTTP server code. For a small application this is probably fine, but it makes the code a little bit more difficult to read and refactor.
It also makes it harder to reuse code. For example, if another route or another part of the app wanted to get a user from the database based on their username, the SELECT
statement above would be duplicated.
So there are a few benefits that we can get from separating the database specific code from the HTTP specific code. Let’s do this by putting all database specific code into a separate javascript file.
Database File
database.js
import mysql from 'mysql2'
const connection = mysql.createPool({
host : 'localhost',
user : 'root',
database : 'some_database'
})
export async function getUser(username) {
const [rows] = await connection.promise().query(`
SELECT *
FROM users
WHERE username = ?
`, [username])
return rows[0]
}
export async function createUser(username, password) {
const result = await connection.promise().query(`
INSERT INTO users (username, password)
VALUES (?, ?)
`,
[username, password]
)
return result.insertId
}
Now we have a database file that only knows about MySQL. It can get a user and created a user but it knows nothing about HTTP or the fact that it’s running as part of an express app.
Then in the app.js file, we can refactor to use this new database.js file.
app.js
import express from 'express'
import database from './database.js'
const app = express()
app.post('/users', async (req, res) => {
const { username, password } = req.body
try {
const user = await database.getUser(username)
if (user) {
res.status(400).send({error: "username already taken"})
return
}
const userId = await database.createUser(username, password)
res.send({ userId })
} catch (error) {
res.sendStatus(500)
return
}
})
export default app
Now app has pretty much no idea that it’s communicating with a mysql database. It just calls the functions it needs to call, and expects the database file to take care of the rest.
But this app.js file is now dependent on the database.js file. It’s tightly coupled to that file, it knows about it, it imports it, and no other part of the app can easily change that. So if we wanted to change the database that’s being used, we would have to modify how the database is imported.
This is really only an issue when it comes to testing. If we want to write unit tests for the express api, it’s tightly coupled to the production database so we’ll also have to test the database. If we can remove this dependency, we’ll be able to test the app.js file in isolation.
So instead of app.js importing it’s dependency, let’s inject it.
Dependency Injection
What would this look like using dependency injection? Well pretty much the same, but with one very important difference:
import express from 'express'
// 1
export default function (database) {
const app = express()
// 2
app.post('/users', async (req, res) => {
// ...
})
// 3
return app
}
- Wrap everything inside a function and export that function from this file instead of the app
- Define all of the routes and other logic inside of the function
- Return what needs to be “exported”
So this is pretty much the same exact code, but everything’s wrapped inside of a function which allows us to pass in any dependencies. Instead of app.js importing the database, the database will be passed to app (injected to app) when it’s created.
The idea here is that app.js has no idea what kind of database it’s going to be using. From the perspective of app.js, it needs to be passed an object, we’ll call it database, and that object must have a getUser
and createUser
method. That’s the agreement. As long as the file that creates teh app passes it a compatible database object, app is going to be happy.
For this to work, we’ll need to update the server.js:
server.js
import database from './database.js'
import makeApp from './app.js'
const app = makeApp(database)
app.listen(8080, () => console.log("listening on port 8080"))
Now the server.js file will import the database and the function from app.js
. Then it will call that function and pass in the database, it’s passing app the dependency.
That’s it. That’s dependency injection.
The Benefit
Let’s just talk about the benefit one more time.
If you take a look at the database.js file, it knows nothing about anything except for making calls to a MySQL database. It’s completely ignorant about the rest of the application.
If you take a look at the app.js file, it knows nothing about anything except for accepting and responding to HTTP request. Well, it also knows that it can call getUser
and createUser
methods on some object. But it knows nothing about that object, it just gets passed to app when it’s created.
server.js knows about all of the different pieces, this is the point in the app that’s “allowed” to know about things. But the different components are mostly ignorant to the rest of the app.
This means that we could make a brand new database file that has a getUser
and createUser
function but uses a mongo database. If server.js imported that database file instead of the MySQL one, it could pass that to the app.js function and the app would be using a mongo database instead of MySQL, and nothing would have to change inside of app.js. app.js has no idea that it’s using a MySQL database, so changing the details about the database doesn’t matter, as long as that database file has a getUser
and a createUser
function.
This is a completely unrealistic example, you’re never just going to completely change the database like this, but you’re in a good place when you can do this. It means that you’ve separated the different parts of your app well.
Why is this good? Because it means we can now test the different parts of the app separately. But that’s a topic for the next article.