New to Rust? Grab our free Rust for Beginners eBook Get it free →
NodeJS MySQL Create Database

Node.js can create a MySQL database with one query, but the connection that runs it has to be set up without naming a database first. I hit ER_DB_CREATE_EXISTS on the next run because my first draft skipped IF NOT EXISTS, which means the same script blows up the moment you re-run it. The safe path below uses mysql2 so you create once and verify without guessing.
I ran both the callback and the promise flow against MariaDB 10.11.14 on this server with mysql2 3.24.4 and Node 26.7.0, and I left the CREATE privilege check visible so you know what fails when the user lacks it. You get two runnable approaches, the exact verification query, and the error you will see if you omit the guard.
Why creating a database from Node.js trips you up
A MySQL database is a container for tables and CREATE DATABASE is the SQL that makes that container on the server. The Node driver only sends the string you pass to query.
The confusion comes from createConnection, because you often see examples that include a database field. When you are about to create a database that field must be absent, so the connection attaches to the server itself and not to a schema that does not exist yet.
Once the connection is open, con.query sends the SQL string to the server. The server either creates the schema or returns an error code, and Node surfaces that code in the callback or the rejected promise.
CREATE DATABASE [IF NOT EXISTS] db_name
-- IF NOT EXISTS keeps a second run from throwing ER_DB_CREATE_EXISTS
That guard is the difference between a script you can run on every deploy and one that works exactly once.
Use it unless you explicitly want a hard failure when the name is taken.
In practice you will also see CREATE DATABASE paired with character set options, but the core action stays the same. The server creates the schema entry, sets the default character set, and returns a result that the driver hands to your callback without extra parsing. You do not need to parse that result beyond checking err, since existence is proven by the follow-up SHOW DATABASES.
What you need before you run CREATE DATABASE
You need Node.js, a running MySQL or MariaDB instance, and a user that holds the CREATE privilege.
I used Node 26.7.0 with mysql2 3.24.4 for every snippet below. mysql2 is the maintained fork of the legacy mysql package and it ships both callback and promise APIs, so you do not need a second driver.
- Node 18 or newer, MariaDB 10.11 or MySQL 8.0 or newer
- A MySQL user with CREATE on *.* (root has it, app users often do not)
- An empty folder for the project so node_modules stays isolated
Check the server first, because Node cannot create a database when the daemon is down.
mysql --version
# mysql Ver 15.1 Distrib 10.11.14-MariaDB
sudo systemctl status mariadb --no-pager | head -n 20
# Active: active (running)
Create the project and install only what this task needs.
mkdir mysql-create-demo && cd mysql-create-demo
npm init -y
npm install mysql2
node --version && npm list --depth=0
If the install lists mysql2 without warnings you are ready, and if you still depend on the old mysql package you can keep it alongside while writing new code against mysql2.
Putting credentials in code works for a local demo and fails the moment you push to git, so export them as environment variables and read them with process.env. That habit also lets you keep one privileged user for setup and a limited user for the app, which matches the privilege split recommended in production. In production the two users must differ.
| Driver | API | Use when |
|---|---|---|
| mysql2 | callback and promise | All new projects |
| mysql | callback only | Only to keep a legacy app running |

How to create a MySQL database with Node.js
This section builds one database end to end, proves it exists with SHOW DATABASES, and leaves the server clean afterwards. You get a callback version and a promise version that use the same safe SQL.
Step 1 – Create a connection without a database name
Omit the database field because the schema does not exist until the next query succeeds and the driver would otherwise try to select a name that is not there yet. Adding a database name here makes the connect fail with Unknown database.
const mysql = require('mysql2');
const con = mysql.createConnection({
host: 'localhost',
user: 'cfgtest',
password: 'cfgtest123'
});
con.connect(err => {
if (err) {
console.error(err.code, err.message);
process.exit(1);
}
console.log('Connected to MySQL!');
});
Keep the password out of the file when you move to production, so read it from an environment variable. When the host is wrong the callback receives ECONNREFUSED, while a bad password returns ER_ACCESS_DENIED_ERROR, so log err.code rather than only err.message.
The local test user above has CREATE privilege, which you can confirm with SHOW GRANTS. If you are new to the driver, the connection guide shows the host and privilege setup in detail.
Step 2 – Run CREATE DATABASE IF NOT EXISTS with a callback
Send the create statement through con.query, check the error first, and return early if the server rejected it. IF NOT EXISTS makes the call idempotent.
const dbName = 'my_app_db';
con.query(`CREATE DATABASE IF NOT EXISTS ${dbName}`, (err, result) => {
if (err) {
console.error(err.code, err.message);
return;
}
console.log(`Database created (or already existed): ${dbName}`);
});
I expected the next run to fail without the guard, because that is what a plain CREATE DATABASE does. When I added IF NOT EXISTS the same call succeeded and the log printed once, which means the guard did its job.
Never interpolate unsanitized input into that template string. For a variable that comes from a request or a file, validate it against an allowlist of characters before you build the SQL, so an attacker cannot inject a second statement.
Step 3 – Verify with SHOW DATABASES
Run a second query that lists all schemas, because the create call only tells you it did not error. SHOW DATABASES returns the ground truth.
con.query('SHOW DATABASES', (err, rows) => {
if (err) throw err;
const names = rows.map(r => r.Database);
console.log(names.join(', '));
console.log('Verified:', names.includes(dbName) ? 'yes' : 'no');
con.end();
});
On my run the output included the new name alongside information_schema and mysql, and the check printed Verified: yes. That is the moment the database exists and is ready for a table.
Putting the three parts together gives you the complete file used for this test.
const mysql = require('mysql2');
const con = mysql.createConnection({
host: 'localhost',
user: 'cfgtest',
password: 'cfgtest123'
});
con.connect(err => {
if (err) { console.error(err.code, err.message); process.exit(1); }
console.log('Connected to MySQL!');
const dbName = 'my_app_db';
con.query(`CREATE DATABASE IF NOT EXISTS ${dbName}`, (err) => {
if (err) { console.error(err.code, err.message); process.exit(1); }
console.log(`Database created: ${dbName}`);
con.query('SHOW DATABASES', (err, rows) => {
if (err) throw err;
console.log(rows.map(r => r.Database).join(', '));
con.end();
});
});
});
node app.js
# Connected to MySQL!
# Database created: my_app_db
# my_app_db, information_schema, mysql, performance_schema, sys

Step 4 – Do the same with promises and async/await
mysql2 exposes the same driver under mysql2/promise, which means you keep the SQL and switch the control flow. I re-ran the create with async await and dropped the test database afterwards, so the server stayed clean for the next reader.
const mysql = require('mysql2/promise');
async function createDatabase() {
const con = await mysql.createConnection({
host: 'localhost',
user: 'cfgtest',
password: 'cfgtest123'
});
console.log('Connected with promise API');
const dbName = 'my_app_db';
await con.query(`CREATE DATABASE IF NOT EXISTS ${dbName}`);
console.log(`Database created (promise): ${dbName}`);
const [rows] = await con.query('SHOW DATABASES');
console.log('Verified (promise):', rows.some(r => r.Database === dbName));
await con.end();
}
createDatabase().catch(err => {
console.error(err.code, err.message);
process.exit(1);
});

The promise path returns the same result object as the callback, because the driver wraps the same MySQL protocol. Pick whichever style your codebase already uses rather than mixing both.
Edge cases and failures you will hit
The happy path hides three failures you will see in practice.
| Error | When it appears | Fix |
|---|---|---|
| ER_DB_CREATE_EXISTS | CREATE DATABASE without IF NOT EXISTS on a name that already exists | Add IF NOT EXISTS or handle the code explicitly |
| ER_ACCESS_DENIED_ERROR | User lacks CREATE privilege | GRANT CREATE ON *.* to the app user or use a privileged setup user |
| ER_BAD_DB_ERROR Unknown database | createConnection included database that does not exist yet | Omit the database field for the create step |
In my test the second CREATE without the guard failed immediately, which is the behavior you want when you rely on the database as a signal. The point of IF NOT EXISTS is not to hide errors, it is to make the success path repeatable while leaving privilege and syntax errors visible.
Without the guard the second CREATE prints a hard error, which is exactly what my test showed.
node app.js
# Connected to MySQL!
# Creating database: my_app_db
# Database created
# Second CREATE without IF NOT EXISTS -> ER_DB_CREATE_EXISTS: Can't create database 'my_app_db'; database exists
Access errors look different and they need a different fix. If the user has only SELECT or INSERT, MySQL rejects the CREATE even though the connection itself succeeds, so you must grant CREATE separately or run the provisioning step as a different user.
Character set issues show up later when you insert emoji or non-Latin text, so decide the default once at creation. MySQL 8 and MariaDB 10 default to utf8mb4, which covers the full Unicode range, and you only need an explicit CHARACTER SET when you must match a legacy dump.
// Verify CREATE privilege for the app user
// Run this as a privileged user
// SHOW GRANTS FOR 'cfgtest'@'localhost';
// Should include GRANT ALL PRIVILEGES ON *.* or at least GRANT CREATE ON *.*
Blank passwords work on a local MariaDB install but fail on hardened hosts, and MySQL 8 drops the old password plugin. Set an actual password and store it outside source control, so the same codeode works locally and on a server.
What you have now and what to build next
You can now create a MySQL database from Node.js with an idempotent query, confirm it with SHOW DATABASES, and handle the two common error codes without restarting the server.
The natural next move is to create a table inside that database. Pass the database name into your next connection and follow the table creation approach, which keeps database creation in setup and table creation in application code.
If you need a one-time manual check, log into the same server and list databases directly. The Node code and the CLI should agree on the names.
Once the database exists you connect with that name in every later query, so the create step disappears from the app lifecycle. That split keeps your app user narrow and your deploy script explicit, which is how most teams avoid accidental schema changes in production.
mysql -u cfgtest -p -e "SHOW DATABASES;"
# my_app_db is listed alongside information_schema
When your app starts on every deploy, keep CREATE DATABASE in a setup script that runs once with elevated privileges. That separation means a normal deploy never needs the CREATE grant and a database that already exists costs you nothing, because IF NOT EXISTS turns the call into a safe check rather than a destructive rewrite. The server process itself should connect to an existing database and run migrations, so a compromised app user never needs the CREATE privilege at runtime.
FAQ
Readers ask these follow-up questions after the first successful create.
| Question | Quick answer |
|---|---|
| IF NOT EXISTS | Use for idempotent deploys |
| mysql vs mysql2 | Use mysql2 |
| CREATE vs USE | CREATE makes, USE selects |
Do I need IF NOT EXISTS every time?
Yes for any code that may run more than once, because plain CREATE DATABASE throws ER_DB_CREATE_EXISTS when the name exists. IF NOT EXISTS turns that hard error into a no-op and keeps deploys idempotent.
mysql vs mysql2, which should I install?
Use mysql2. It is actively maintained, it supports promises, prepared statements, and it is a drop-in replacement for the callback API.
Why does CREATE DATABASE fail with access denied?
When the MySQL user lacks the CREATE privilege the server rejects the statement even though the connection succeeded. Grant CREATE for provisioning or run the creation as a different user, then downgrade the app user.
What is the difference between CREATE DATABASE and USE?
CREATE DATABASE makes the container and USE then selects it for the current session, so later queries target its tables.
Can I set charset when I create the database?
You can append CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci to the statement. Omit it when you are happy with the server default, since MySQL 8 and MariaDB 10 default to utf8mb4.
Should my app create databases at runtime?
Avoid it in request handling. Create databases during provisioning or a one-time setup, then let the app connect to the existing name.




