node-mssql: a promise-based SQL Server client built on TDS drivers
Microsoft SQL Server client for Node.js
At a glance
- What is it?
- The mssql package wraps one of two TDS drivers behind a single pooled API with tagged template literals, input and output parameters and prepared statements. What the release history shows is a project that spends its effort on identifier safety.
- Who is it for?
- Choose node-mssql when your Node service talks to SQL Server over TDS and you want one API across both a pure JavaScript and a native driver, with escaping and identifier handling done for you rather than assembled by hand. Look elsewhere when you need a database other than SQL Server, when a native dependency on Windows or Linux 64-bit is unacceptable, or when you want the ORM features that something like TypeORM puts on top instead of a driver-level client.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 6 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 8, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Installing mssql and choosing between the two TDS drivers
The install is one command for the default path:
npm install mssqlThe driver choice is the decision that actually matters, and it comes first because it determines whether you have a native dependency. The README lists Tedious as the default, describing it as pure JavaScript running on Windows, macOS and Linux. That description is the reason to prefer it: no compiler, no platform-specific build, no rebuild step in your container image.
The alternative is MSNodeSQLv8, the Microsoft and contributors native Node V8 driver for SQL Server, which the README marks as v2 through v5 and limited to Windows or 64-bit Linux and macOS. It goes in alongside the package:
npm install mssql msnodesqlv8What you get for the native dependency is Windows Authentication, which the pure JavaScript driver does not offer. That is the trade in one sentence: cross-platform simplicity against integrated Windows auth. Everything else in the API stays the same, because the driver sits underneath a shared request and pool layer.
One prerequisite is stated plainly and is worth internalising before you debug anything: the package connects over TCP/IP, so your SQL Server installation may need that protocol enabled. A connection refused on a fresh server is usually this, not the driver.
The shortest useful program is a connection string and one query
The README's short example connects with a raw connection string and runs a tagged template literal query:
const sql = require('mssql')
(async () => {
try {
// make sure that any items are correctly URL encoded in the connection string
await sql.connect('Server=localhost,1433;Database=database;User Id=username;Password=password;Encrypt=true')
const result = await sql.query`select * from mytable where id = ${value}`
console.dir(result)
} catch (err) {
// ... error checks
}
})()Two details in that snippet carry most of the design. First, `sql.query` is a tagged template, so the interpolated value is bound as a parameter rather than pasted into the SQL text. That is the default you want and it is why the injection section exists further down the README as a topic worth documenting at all.
Second, connecting is a module-level operation that returns the pool, and `sql.query` is available directly on the module rather than on a request object. That shortcut is convenient and it is also the thing you outgrow: once you need input parameters, output parameters or per-query type information, you build a request from the pool instead.
The comment about URL encoding is worth taking literally even though the example uses a semicolon-separated connection string rather than a URL. Parts containing characters that break parsing need encoding, and a password with an ampersand in it is the classic case.
The config object, pool sizing and the encryption flags
The longer example moves to a config object, which is what you want in anything real because the values come from environment variables rather than from source:
const sql = require('mssql')
const sqlConfig = {
user: process.env.DB_USER,
password: process.env.DB_PWD,
database: process.env.DB_NAME,
server: 'localhost',
pool: {
max: 10,
min: 0,
idleTimeoutMillis: 30000
},
options: {
encrypt: true, // for azure
trustServerCertificate: false // change to true for local dev / self-signed certs
}
}The pool block is the part people copy without reading and then get wrong in production. `max` is a ceiling on connections to the server, `min` is the floor kept warm, and `idleTimeoutMillis` is how long an unused connection is held before it is returned. A min of 0 means the first query after an idle period pays for a fresh TCP connection, which is fine for a CLI and wrong for a latency sensitive service.
The two option flags are a genuine fork in the road for anyone connecting to Azure. `encrypt: true` turns on transport encryption, and `trustServerCertificate: false` means the server certificate must validate against a trusted authority. Turn that second flag to true and you accept any certificate, which is fine for a laptop with a self-signed cert and wrong on a network where someone else might be in the middle. The README's own comment says exactly that: change to true for local dev or self-signed certs.
Named instances are handled in the server string, which the README shows as a localhost value with a backslash-separated instance name, so the same field covers both default and named instances.
Requests, typed parameters and stored procedures
Everything that needs a parameter, a type or a transaction goes through a request object from the pool. The README's async and await example shows the full shape, including a stored procedure with an output parameter:
const sql = require('mssql')
(async function () {
try {
let pool = await sql.connect(config)
let result1 = await pool.request()
.input('input_parameter', sql.Int, value)
.query('select * from mytable where id = @input_parameter')
console.dir(result1)
// Stored procedure
let result2 = await pool.request()
.input('input_parameter', sql.Int, value)
.output('output_parameter', sql.VarChar(50))
.execute('procedure_name')
console.dir(result2)
} catch (err) {
// ... error checks
}
})()The method chain is the API's shape: input, output, replaceInput, replaceOutput, then query, execute, batch, bulk or cancel. Notice the SQL text uses an at-sign parameter, `@input_parameter`, which is T-SQL convention and not an mssql feature. The binding is done by the driver.
Types come from the package, as `sql.Int` and `sql.VarChar(50)` here. Declaring the type rather than letting the server infer it matters for output parameters and for query plans, because an inferred type can differ from what you meant.
The snippet also has a loose end worth flagging, since it will look familiar and then confuse you: it references `config` and `value` without defining either. The README assumes you already have a config object and a value in scope. The same applies to `sql.on('error', ...)` in the promises example, which is the package-level error handler and a separate thing from the try and catch around a query. Registering it is worthwhile in long-running services, because pool-level errors surface there rather than on a request you are awaiting.
Identifier rules, and a 12.7.2 release that is mostly hardening
The README carries a section on identifier rules covering parameter names, column names, type names and sizes, and procedure names. That section is not decoration. The v12.7.2 release is a bug fix release and almost every entry in it is about refusing malformed identifiers rather than about features.
The fixes read as a single theme. One rejects column names that break out of quoted identifiers. One rejects type metadata that escapes the parameter declaration. One validates parameter names against an allowlist. Another stops a bulk insert continuing after an invalid table name, and another validates bulk ordering keys. Two more are resource-shaped rather than injection-shaped: releasing the connection when a bulk load's options are rejected, and removing request listeners when a request is abandoned before it is sent.
The practical reading is that identifier handling is treated as a security boundary, not a convenience. If your application builds column or type names dynamically, this is the release line that will start rejecting work rather than executing it, and the error is a hard failure at prepare time. Pinning to a 12.x release is the safer side of that change.
The same seven fixes appear in the v10.0.5 and v11.0.2 releases published on the same day, which is the maintainers backporting a security-relevant set of changes across supported lines rather than only moving the newest major forward. For an upgrade, that is the good case: the patch landed on every branch you might still be pinned to.
The manifest says 9.1.1 while the releases are on 12.x
The package.json in the repository declares its version as 9.1.1. The three most recent releases on the repository are v12.7.2, v11.0.2 and v10.0.5, each published on 2026-09-10. Both facts are recorded, and the interesting part is that they do not sit comfortably together.
The rest of the same manifest argues for the newer number. It requires Node 18.19.0 or later, accepts either tedious 19 or 20, and its development dependencies include mocha 11, semantic-release 25 and commitlint 20. Those are current packages, and a manifest carrying them is not plausibly the manifest of a 9.1.1 release from years earlier. The version string and the dependency ranges in that file describe different moments.
There is a mechanical explanation available, and it is the one to rely on. The repository uses semantic-release, which is configured in .releaserc and in the development dependencies, and that tool writes the version number at publish time on a release commit. A manifest read from a working branch rather than from a tagged release can therefore carry a stale string even when everything around it is current. The repository also keeps a CHANGELOG.txt and a `.commitlintrc`, which fit that setup.
The action follows from this: resolve the version your lockfile actually installs rather than reading it from a manifest, and treat the releases page as the record of what shipped. If you need to know what a given major changed, the README indexes the migration notes, with pages listed for 11.x to 12.x, 10.x to 11.x, 9.x to 10.x and back through 3.x to 4.x, which is a longer chain of documented upgrades than most libraries of this size bother to keep.
What the README covers, and what it hands off to the hosted docs
The README is strongest on shape. It installs the package, connects with a string, connects with a config object, and shows the request chain for a query and a stored procedure. Four shapes cover most day-to-day work.
Everything past that is a table of contents pointing at detail: streaming, connection pools and validation, the ConnectionPool class and its parseConnectionString, prepared statements with prepare, execute and unprepare, transactions with begin, commit and rollback, a CLI, geography and geometry types, table-valued parameters, response schema, affected rows, JSON support, duplicate column name handling, informational messages, metadata, data types, the diagnostics channel, and the SQL injection section. The hosted documentation at tediousjs.github.io/node-mssql is where those sections are written, and the links in the README resolve to it.
The Windows Authentication example is the one place the README shows an option the default driver does not use, and it is a good illustration of the split between the two drivers. It requires the msnodesqlv8 entry point and sets `trustedConnection: true` for Windows Authentication, alongside `trustServerCertificate: true` for self-signed certificates. It also carries a commented line for pinning a specific ODBC driver, which is how you control which native driver version is used.
One inconsistency is worth knowing about before you follow the Azure advice literally. The README says that on Windows Azure you should add `?encrypt=true` to the connection string, using a question mark. The short example directly above it is a semicolon-separated string that already sets `Encrypt=true`. Those are two different connection string styles, and the query-string form belongs to the URL style rather than the ADO style used in the example. Set `encrypt: true` in the options block, as the config example does, and ignore the question mark hint if your configuration is semicolon-separated.
Editorial conclusion
Choose node-mssql when your Node service talks to SQL Server over TDS and you want one API across both a pure JavaScript and a native driver, with escaping and identifier handling done for you rather than assembled by hand. Look elsewhere when you need a database other than SQL Server, when a native dependency on Windows or Linux 64-bit is unacceptable, or when you want the ORM features that something like TypeORM puts on top instead of a driver-level client. Before you pin anything, check the version your lockfile resolves rather than the number in the repository manifest, decide on Tedious or MSNodeSQLv8 deliberately, and confirm your server has TCP/IP enabled, which is the single most common first-run failure.
Frequently asked questions
Can I use node with SQL?
Yes, through a driver, and node-mssql is the SQL Server one. It connects over TDS, which the README says requires TCP/IP to be enabled on the SQL Server installation, and it exposes the connection through either a connection string or a config object with pool and encryption options.
How do I run a parameterized query with node-mssql?
Use a request from the pool and declare the parameter with a type. The README chains input, then query, for example pool.request().input('input_parameter', sql.Int, value).query('select * from mytable where id = @input_parameter'). The at-sign name is the T-SQL parameter and the driver binds the value.
Tedious or MSNodeSQLv8, and does either need a native build?
Tedious does not. It is described as pure JavaScript running on Windows, macOS and Linux, and it is the default. MSNodeSQLv8 is the native option, covering v2 to v5 and restricted to Windows or 64-bit Linux and macOS, and it is the one that supports Windows Authentication through trustedConnection.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/tediousjs-node-mssql)