basic-ftp vs ftp vs ftp-deploy vs ssh2-sftp-client
File Transfer Protocols in Node.js Build Pipelines
basic-ftpftpftp-deployssh2-sftp-clientSimilar Packages:

File Transfer Protocols in Node.js Build Pipelines

basic-ftp, ftp, ftp-deploy, and ssh2-sftp-client are Node.js libraries used for transferring files between a local machine and a remote server. basic-ftp and ftp handle standard FTP and FTPS connections, with basic-ftp offering modern Promise-based APIs while ftp relies on older callback patterns. ssh2-sftp-client is distinct because it handles SFTP (SSH File Transfer Protocol), which is more secure than standard FTP. ftp-deploy is a higher-level utility designed specifically for syncing directory structures, often used in deployment scripts rather than general-purpose file manipulation.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
basic-ftp0712138 kB165 days agoMIT
ftp01,134-14011 years ago-
ftp-deploy028729.8 kB352 years agoMIT
ssh2-sftp-client0911244 kB48 months agoApache-2.0

File Transfer in Node.js: FTP vs SFTP and Modern vs Legacy

When automating file transfers in Node.js, you generally face two major decisions: which protocol to use (FTP or SFTP) and which library abstraction fits your workflow. The packages basic-ftp, ftp, ftp-deploy, and ssh2-sftp-client represent different points on this spectrum. Let's break down how they handle connections, security, and directory management.

πŸ”’ Protocol Security: FTPS vs SFTP

Security is the biggest differentiator here. Standard FTP sends data in plain text, while SFTP encrypts everything over SSH.

basic-ftp supports FTP and explicit FTPS (FTP over SSL/TLS).

  • You must explicitly enable security in the options.
  • Good for legacy servers that don't support SSH.
// basic-ftp: Enabling FTPS
const client = new ftp.Client()
client.ftp.secure = true
await client.access({
  host: "ftp.example.com",
  user: "user",
  password: "password"
})

ftp also supports FTP and FTPS but uses an event-driven model.

  • Security configuration is similar but requires more manual setup.
  • Less intuitive for modern async/await code.
// ftp: Enabling FTPS
const client = new ftp.Client()
client.on('ready', () => { /* ... */ })
client.connect({
  host: "ftp.example.com",
  secure: true,
  user: "user",
  password: "password"
})

ssh2-sftp-client uses SFTP exclusively.

  • Security is built-in via SSH keys or password over SSH.
  • Required for most modern cloud hosting and compliant environments.
// ssh2-sftp-client: SFTP Connection
const client = new SftpClient()
await client.connect({
  host: "sftp.example.com",
  username: "user",
  password: "password",
  port: 22
})

ftp-deploy typically wraps an FTP client.

  • Focuses on the deployment task rather than the protocol details.
  • Usually configured for FTPS if security is needed.
// ftp-deploy: Configuring secure deployment
const config = {
  user: "user",
  password: "password",
  host: "ftp.example.com",
  protocol: "ftps" // or 'ftp'
}
await ftpDeploy.deploy(config)

πŸ”„ Async Handling: Promises vs Callbacks

Modern Node.js development relies heavily on async/await. The library you choose dictates how clean your code looks.

basic-ftp is built with Promises from the ground up.

  • Every method returns a Promise.
  • Error handling uses standard try/catch blocks.
  • No need for external promisification libraries.
// basic-ftp: Native Promise support
try {
  await client.upload("local.txt", "remote.txt")
} catch (err) {
  console.error("Upload failed", err)
}

ftp uses callbacks and events.

  • You often need to wrap methods in Promises manually.
  • Code can become nested or require utility libraries like util.promisify.
// ftp: Callback based
client.put("local.txt", "remote.txt", (err) => {
  if (err) throw err
  console.log("Upload complete")
})

ssh2-sftp-client provides a Promise-based API over ssh2.

  • Similar experience to basic-ftp but for SFTP.
  • Makes secure transfers as easy as insecure ones.
// ssh2-sftp-client: Promise based
try {
  await client.put("local.txt", "remote.txt")
} catch (err) {
  console.error("SFTP error", err)
}

ftp-deploy is Promise-based.

  • Designed for high-level scripts.
  • Hides the connection lifecycle entirely.
// ftp-deploy: High level promise
try {
  await ftpDeploy.deploy(config)
} catch (err) {
  console.error("Deployment failed", err)
}

πŸ“‚ Directory Syncing: Manual vs Automatic

Uploading a single file is easy. Uploading a whole project folder with changed files only is harder.

basic-ftp requires manual recursion.

  • You must write logic to list directories and loop through files.
  • Gives you full control but adds code complexity.
// basic-ftp: Manual directory upload
async function uploadDir(localPath, remotePath) {
  const files = await fs.readdir(localPath)
  for (const file of files) {
    await client.upload(`${localPath}/${file}`, `${remotePath}/${file}`)
  }
}

ftp also requires manual recursion.

  • Similar to basic-ftp but with callback hell potential.
  • You manage the file traversal logic yourself.
// ftp: Manual directory upload
function uploadDir(localPath, remotePath, cb) {
  fs.readdir(localPath, (err, files) => {
    // Iterate and call client.put for each
  })
}

ssh2-sftp-client requires manual recursion.

  • Like the FTP clients, it focuses on file operations, not syncing.
  • You need to combine it with a library like fast-glob for efficient syncing.
// ssh2-sftp-client: Manual directory upload
async function uploadDir(localPath, remotePath) {
  const files = await fs.readdir(localPath)
  for (const file of files) {
    await client.put(`${localPath}/${file}`, `${remotePath}/${file}`)
  }
}

ftp-deploy handles syncing automatically.

  • It compares local and remote timestamps or hashes.
  • Only uploads files that have changed.
  • Best for deploying builds or static sites.
// ftp-deploy: Automatic sync
const config = {
  local: "./dist",
  remote: "/public/html",
  deleteRemote: false // Keep old files or clean up
}
await ftpDeploy.deploy(config)

πŸ› οΈ Maintenance and Ecosystem

The longevity of your dependency matters for security patches and Node.js compatibility.

basic-ftp is actively maintained.

  • Regularly updated for Node.js compatibility.
  • Considered the standard for FTP in modern Node projects.
  • Recommended by the community as the successor to ftp.

ftp is effectively legacy.

  • Rarely receives updates.
  • May have compatibility issues with newer Node.js versions.
  • Should be migrated away from in new architectures.

ssh2-sftp-client is actively maintained.

  • Critical for teams requiring SSH security.
  • Depends on ssh2, which is also well-maintained.
  • Standard choice for SFTP needs.

ftp-deploy is niche but stable.

  • Maintained for its specific use case.
  • Good for CI/CD pipelines where you don't want to write sync logic.
  • Less flexible if you need custom file manipulation logic.

πŸ“Š Summary: Key Differences

Featurebasic-ftpftpssh2-sftp-clientftp-deploy
ProtocolFTP / FTPSFTP / FTPSSFTP (SSH)FTP / FTPS
API StylePromisesCallbacksPromisesPromises
Sync LogicManualManualManualAutomatic
Statusβœ… Active⚠️ Legacyβœ… Activeβœ… Stable
Best ForCustom FTP scriptsLegacy maintenanceSecure transfersDeployment pipelines

πŸ’‘ The Big Picture

basic-ftp is your go-to for general FTP tasks in modern code. It balances control with ease of use. If you are building a tool that needs to interact with an FTP server, start here.

ssh2-sftp-client is the security-first choice. If your infrastructure team disables FTP ports and only allows SSH, this is the only option on this list that will work. Treat it as the SFTP equivalent of basic-ftp.

ftp-deploy saves time for specific jobs. Don't use it if you need to download files or manipulate them on the server. Do use it if you just want to push a build folder to a web host every time you run CI.

ftp should be avoided. It exists mostly for historical reasons. Using it introduces technical debt immediately.

Final Thought: The choice often comes down to security requirements first (SFTP vs FTP), and then workflow needs (Sync vs Custom Script). Always prefer SFTP if the server supports it, and always prefer Promise-based libraries for maintainable code.

How to Choose: basic-ftp vs ftp vs ftp-deploy vs ssh2-sftp-client

  • basic-ftp:

    Choose basic-ftp if you need a modern, actively maintained FTP/FTPS client with native Promise support. It is the recommended replacement for the older ftp package and works well for custom scripts where you need fine-grained control over connections without the complexity of callbacks.

  • ftp:

    Avoid choosing ftp for new projects as it is largely considered legacy and unmaintained. Only use this if you are maintaining an older codebase that already depends on it and refactoring to basic-ftp is not currently feasible due to resource constraints.

  • ftp-deploy:

    Choose ftp-deploy if your goal is specifically to sync a local folder to a remote server for deployment purposes. It abstracts away the connection logic and focuses on diffing and uploading changed files, making it ideal for CI/CD pipelines or simple static site uploads.

  • ssh2-sftp-client:

    Choose ssh2-sftp-client if your server requires SFTP instead of standard FTP. This is critical for security compliance, as SFTP encrypts both commands and data. Use this when working with secure infrastructure that disables plain FTP access.

README for basic-ftp

Basic FTP

npm version npm downloads Node.js CI

This is an FTP client library for Node.js. It supports FTPS over TLS, Passive Mode over IPv6, has a Promise-based API, and offers methods to operate on whole directories. Active Mode is not supported.

Advisory

Prefer alternative transfer protocols like HTTPS or SFTP (SSH). FTP is a an old protocol with some reliability issues. Use this library when you have no choice and need to use FTP. Try to use FTPS (FTP over TLS) whenever possible, FTP alone does not provide any security.

Dependencies

Node 10.0 or later is the only dependency.

Installation

npm install basic-ftp

Usage

The first example will connect to an FTP server using TLS (FTPS), get a directory listing, upload a file and download it as a copy. Note that the FTP protocol doesn't allow multiple requests running in parallel.

const { Client } = require("basic-ftp") 
// ESM: import { Client } from "basic-ftp"

example()

async function example() {
    const client = new Client()
    client.ftp.verbose = true
    try {
        await client.access({
            host: "myftpserver.com",
            user: "very",
            password: "password",
            secure: true
        })
        console.log(await client.list())
        await client.uploadFrom("README.md", "README_FTP.md")
        await client.downloadTo("README_COPY.md", "README_FTP.md")
    }
    catch(err) {
        console.log(err)
    }
    client.close()
}

The next example deals with directories and their content. First, we make sure a remote path exists, creating all directories as necessary. Then, we make sure it's empty and upload the contents of a local directory.

await client.ensureDir("my/remote/directory")
await client.clearWorkingDir()
await client.uploadFromDir("my/local/directory")

If you encounter a problem, it may help to log out all communication with the FTP server.

client.ftp.verbose = true

Client API

new Client(timeout, options)

Create a client instance. Configure it with a timeout in milliseconds that will be used for any connection made. Use 0 to disable timeouts, default is 30 seconds. Options are:

  • allowSeparateTransferHost (boolean), the FTP spec makes it possible for a server to tell the client to use a different IP address for file transfers than for the initial control connection. Today, this feature is very rarely used. Still, the default for this is set to true for backwards-compatibility reasons. If you experience any issues with NAT traversal in local networks or want to provide more security and prevent FTP bounce attacks, set this to false.

close()

Close the client and any open connection. The client can’t be used anymore after calling this method, you'll have to reconnect with access to continue any work. A client is also closed automatically if any timeout or connection error occurs. See the section on Error Handling below.

closed

True if the client is not connected to a server. You can reconnect with access.

access(options): Promise<FTPResponse>

Get access to an FTP server. This method will connect to a server, optionally secure the connection with TLS, login a user and apply some default settings (TYPE I, STRU F, PBSZ 0, PROT P). It returns the response of the initial connect command. This is an instance method and thus can be called multiple times during the lifecycle of a Client instance. Whenever you do, the client is reset with a new connection. This also implies that you can reopen a Client instance that has been closed due to an error when reconnecting with this method. The available options are:

  • host (string) Server host, default: localhost
  • port (number) Server port, default: 21
  • user (string) Username, default: anonymous
  • password (string) Password, default: guest
  • secure (boolean | "implicit") Explicit FTPS over TLS, default: false. Use "implicit" if you need support for legacy implicit FTPS.
  • secureOptions Options for TLS, same as for tls.connect() in Node.js.

features(): Promise<Map<string, string>>

Get a description of supported features. This will return a Map where keys correspond to FTP commands and values contain further details. If the FTP server doesn't support this request you'll still get an empty Map instead of an error response.

send(command): Promise<FTPResponse>

Send an FTP command and return the first response.

sendIgnoringError(command): Promise<FTPResponse>

Send an FTP command, return the first response, and ignore an FTP error response. Any other error or timeout will still reject the Promise.

cd(path): Promise<FTPResponse>

Change the current working directory.

pwd(): Promise<string>

Get the path of the current working directory.

list([path]): Promise<FileInfo[]>

List files and directories in the current working directory, or at path if specified. Currently, this library only supports MLSD, Unix and DOS directory listings. See FileInfo for more details.

lastMod(path): Promise<Date>

Get the last modification time of a file. This command might not be supported by your FTP server and throw an exception.

size(path): Promise<number>

Get the size of a file in bytes.

rename(path, newPath): Promise<FTPResponse>

Rename a file. Depending on the server you may also use this to move a file to another directory by providing full paths.

remove(path): Promise<FTPResponse>

Remove a file.

uploadFrom(readableStream | localPath, remotePath, [options]): Promise<FTPResponse>

Upload data from a readable stream or a local file to a remote file. If such a file already exists it will be overwritten. If a file is being uploaded, additional options offer localStart and localEndInclusive to only upload parts of it.

appendFrom(readableStream | localPath, remotePath, [options]): Promise<FTPResponse>

Upload data from a readable stream or a local file by appending it to an existing file. If the file doesn't exist the FTP server should create it. If a file is being uploaded, additional options offer localStart and localEndInclusive to only upload parts of it. For example: To resume a failed upload, request the size of the remote, partially uploaded file using size() and use it as localStart.

downloadTo(writableStream | localPath, remotePath, startAt = 0): Promise<FTPResponse>

Download a remote file and pipe its data to a writable stream or to a local file. You can optionally define at which position of the remote file you'd like to start downloading. If the destination you provide is a file, the offset will be applied to it as well. For example: To resume a failed download, request the size of the local, partially downloaded file and use that as startAt.


ensureDir(remoteDirPath): Promise<void>

Make sure that the given remoteDirPath exists on the server, creating all directories as necessary. The working directory is at remoteDirPath after calling this method.

clearWorkingDir(): Promise<void>

Remove all files and directories from the working directory.

removeDir(remoteDirPath): Promise<void>

Remove all files and directories from a given directory, including the directory itself. The working directory stays the same unless it is part of the deleted directories.

uploadFromDir(localDirPath, [remoteDirPath]): Promise<void>

Upload the contents of a local directory to the current remote working directory. This will overwrite existing files with the same names and reuse existing directories. Unrelated files and directories will remain untouched. You can optionally provide a remoteDirPath to put the contents inside any remote directory which will be created if necessary including all intermediate directories. The working directory stays the same after calling this method.

downloadToDir(localDirPath, [remoteDirPath]): Promise<void>

Download all files and directories of the current working directory to a given local directory. You can optionally set a specific remote directory. The working directory stays the same after calling this method.


trackProgress(handler)

Report any transfer progress using the given handler function. See the next section for more details.

Transfer Progress

Set a callback function with client.trackProgress to track the progress of any transfer. Transfers are uploads, downloads or directory listings. To disable progress reporting, call trackProgress without a handler.

// Log progress for any transfer from now on.
client.trackProgress(info => {
    console.log("File", info.name)
    console.log("Type", info.type)
    console.log("Transferred", info.bytes)
    console.log("Transferred Overall", info.bytesOverall)
})

// Transfer some data
await client.uploadFrom(someStream, "test.txt")
await client.uploadFrom("somefile.txt", "test2.txt")

// Set a new callback function which also resets the overall counter
client.trackProgress(info => console.log(info.bytesOverall))
await client.downloadToDir("local/path", "remote/path")

// Stop logging
client.trackProgress()

For each transfer, the callback function will receive the filename, transfer type (upload, download or list) and number of bytes transferred. The function will be called at a regular interval during a transfer.

There is also a counter for all bytes transferred since the last time trackProgress was called. This is useful when downloading a directory with multiple files where you want to show the total bytes downloaded so far.

Error Handling

Any error reported by the FTP server will be thrown as FTPError. The connection to the FTP server stays intact and you can continue to use your Client instance.

This is different with a timeout or connection error: In addition to an Error being thrown, any connection to the FTP server will be closed. You’ll have to reconnect with client.access(), if you want to continue any work.

Logging

Using client.ftp.verbose = true will log debug-level information to the console. You can use your own logging library by overriding client.ftp.log. This method is called regardless of what client.ftp.verbose is set to. For example:

myClient.ftp.log = myLogger.debug

Static Types

In addition to unit tests and linting, the source code is written in Typescript using rigorous compiler settings like strict and noImplicitAny. When building the project, the source is transpiled to Javascript and type declaration files. This makes the library useable for both Javascript and Typescript projects.

Extending the library

Client

get/set client.parseList

Provide a function to parse directory listing data. This library supports MLSD, Unix and DOS formats. Parsing these list responses is one of the more challenging parts of FTP because there is no standard that all servers adhere to. The signature of the function is (rawList: string) => FileInfo[].

FTPContext

The Client API described so far is implemented using an FTPContext. An FTPContext provides the foundation to write an FTP client. It holds the socket connections and provides an API to handle responses and events in a simplified way. Through client.ftp you get access to this context.

get/set verbose

Set the verbosity level to optionally log out all communication between the client and the server.

get/set encoding

Set the encoding applied to all incoming and outgoing messages of the control connection. This encoding is also used when parsing a list response from a data connection. See https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings for what encodings are supported by Node.js. Default is utf8 because most modern servers support it, some of them without mentioning it when requesting features.

Acknowledgment

This library uses parts of the directory listing parsers written by The Apache Software Foundation. They've been made available under the Apache 2.0 license. See the included notice and headers in the respective files containing the original copyright texts and a description of changes.