basic-ftp vs ftp vs node-ssh vs ssh2-sftp-client
Secure File Transfer and Remote Execution in Node.js
basic-ftpftpnode-sshssh2-sftp-clientSimilar Packages:

Secure File Transfer and Remote Execution in Node.js

basic-ftp, ftp, node-ssh, and ssh2-sftp-client are Node.js libraries designed for interacting with remote servers, but they target different protocols and use cases. basic-ftp and ftp handle the legacy FTP/FTPS protocols for simple file transfers, while node-ssh and ssh2-sftp-client leverage the secure SSH protocol for both command execution and file management. basic-ftp is a modern, promise-based FTP client, whereas ftp is an older, event-driven library. On the SSH side, node-ssh provides a high-level, user-friendly wrapper for executing commands and transferring files, while ssh2-sftp-client offers a promise-based interface specifically optimized for SFTP file operations built on top of the robust ssh2 engine.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
basic-ftp0734155 kB11a month agoMIT
ftp01,129-13911 years ago-
node-ssh01,00778.3 kB582 years agoMIT
ssh2-sftp-client0924247 kB16 months agoApache-2.0

Secure File Transfer and Remote Execution: Choosing the Right Node.js Library

When your Node.js application needs to talk to a remote server—whether to upload a build artifact, fetch a log file, or run a deployment script—you generally have two protocol choices: FTP (legacy) or SSH (modern). The packages basic-ftp, ftp, node-ssh, and ssh2-sftp-client solve these problems, but they differ wildly in maintenance status, security, and ease of use. Let's break down how they compare in real-world scenarios.

🚨 Maintenance Status: Active vs. Deprecated

The most critical factor in this comparison is maintenance. Using an unmaintained library for network operations is a major security risk.

ftp is officially deprecated. The repository is archived, and the maintainer explicitly advises against using it in new projects. It relies on older Node.js patterns (callbacks and event emitters) that make error handling tricky in modern async code.

// ftp: Deprecated event-based API
const Client = require('ftp');
const c = new Client();

c.on('ready', () => {
  c.get('remote.txt', (err, stream) => {
    if (err) throw err;
    // Handling streams inside callbacks can get messy
    stream.pipe(require('fs').createWriteStream('local.txt'));
    c.end();
  });
});

c.connect({ host: 'example.com' });

basic-ftp is the modern replacement. It is actively maintained, has zero dependencies, and uses native Promises, making it work seamlessly with async/await.

// basic-ftp: Modern promise-based API
import * as ftp from 'basic-ftp';

async function example() {
  const client = new ftp.Client();
  client.ftp.verbose = true;
  
  try {
    await client.access({
      host: 'example.com',
      user: 'user',
      password: 'password',
      secure: true // FTPS
    });
    
    await client.downloadTo('local.txt', 'remote.txt');
  } catch (err) {
    console.error(err);
  }
  client.close();
}

node-ssh and ssh2-sftp-client are both actively maintained and built on the secure SSH protocol. They are safe choices for new development, with node-ssh focusing on general usability and ssh2-sftp-client focusing on file transfer robustness.

🔐 Security: Plain Text vs. Encrypted Tunnels

FTP sends data—including passwords—in plain text unless explicitly upgraded to FTPS. SSH encrypts everything by default.

basic-ftp supports FTPS (FTP over SSL/TLS), which encrypts the control channel and optionally the data channel. However, it still operates on the older FTP architecture, which can be problematic with strict firewalls due to separate data ports.

// basic-ftp: Enforcing secure FTPS
await client.access({
  host: 'secure-ftp.example.com',
  user: 'user',
  password: 'secret',
  secure: true, // Requires TLS
  secureOptions: { rejectUnauthorized: false }
});

node-ssh and ssh2-sftp-client use SSH, which is the industry standard for secure remote access. They support key-based authentication, which is far more secure than passwords and essential for automated scripts.

// node-ssh: Key-based authentication
import { NodeSSH } from 'node-ssh';

const ssh = new NodeSSH();

await ssh.connect({
  host: 'server.example.com',
  username: 'deploy',
  privateKey: require('fs').readFileSync('/home/user/.ssh/id_rsa')
});
// ssh2-sftp-client: Key-based authentication
import Client from 'ssh2-sftp-client';

const sftp = new Client();

await sftp.connect({
  host: 'server.example.com',
  username: 'deploy',
  privateKey: require('fs').readFileSync('/home/user/.ssh/id_rsa')
});

📤 File Transfer: Simplicity vs. Control

How you move files depends on whether you prioritize simple one-liners or fine-grained stream control.

basic-ftp offers high-level methods like uploadFrom and downloadTo. It handles the complexity of FTP data connections internally, which is great for simple scripts but less flexible if you need to pipe streams directly.

// basic-ftp: High-level file upload
await client.uploadFrom('local-build.zip', 'remote-build.zip');

node-ssh provides very convenient putFile and getFile methods that feel like standard file system operations. It abstracts away the SFTP protocol details completely.

// node-ssh: Simple file put
await ssh.putFile('/local/path/config.json', '/remote/path/config.json');

ssh2-sftp-client also offers put and get, but it shines when you need to work with streams or check specific file attributes during transfer. It gives you slightly more visibility into the SFTP session.

// ssh2-sftp-client: Stream-based upload
const stream = sftp.createWriteStream('/remote/path/large-file.bin');
stream.end(require('fs').readFileSync('large-file.bin'));

await new Promise((resolve, reject) => {
  stream.on('close', resolve);
  stream.on('error', reject);
});

⚙️ Command Execution: The SSH Advantage

FTP is strictly for files. If you need to run commands (like restarting a service or navigating directories dynamically), you must use SSH.

basic-ftp and ftp cannot execute shell commands. They are limited to file operations and directory listing within the FTP sandbox.

node-ssh excels here. It provides a dedicated exec() method that returns stdout and stderr, making it perfect for deployment logic.

// node-ssh: Executing remote commands
const result = await ssh.execCommand('npm install && npm run build', {
  cwd: '/var/www/my-app'
});

console.log(result.stdout);
if (result.code !== 0) {
  console.error(result.stderr);
}

ssh2-sftp-client is primarily focused on SFTP (file transfer). While it is built on the ssh2 library which can execute commands, ssh2-sftp-client itself does not expose a high-level exec method in its main API. For command execution, you would need to drop down to the underlying ssh2 client or choose node-ssh.

// ssh2-sftp-client: No direct exec method in high-level API
// You use it for file operations like list(), get(), put()
const list = await sftp.list('/remote/path');
console.log(list); 
// To run commands, you'd typically switch to node-ssh or raw ssh2

🧩 Error Handling and Developer Experience

Modern development relies on try...catch blocks. Older libraries force you into callback hell or complex event listener chains.

ftp requires attaching listeners for every possible state (ready, error, close). Missing one can cause your process to hang indefinitely.

basic-ftp, node-ssh, and ssh2-sftp-client all return Promises. This means you can use standard try...catch blocks, ensuring that connection errors, timeouts, or permission issues are handled gracefully without crashing your server.

// All modern packages support this pattern:
try {
  await client.connect(config);
  await client.doWork();
} catch (error) {
  // Handles network errors, auth failures, and timeouts uniformly
  console.error('Operation failed:', error.message);
} finally {
  await client.end(); // Ensure connection is closed
}

📊 Summary: Which One Fits Your Needs?

Featurebasic-ftpftpnode-sshssh2-sftp-client
ProtocolFTP / FTPSFTP / FTPSSSH / SFTPSSH / SFTP
Status✅ Active❌ Deprecated✅ Active✅ Active
API StylePromisesEvents/CallbacksPromisesPromises
File TransferExcellentGoodGoodExcellent
Command Exec❌ No❌ No✅ Yes⚠️ Limited (Focus on SFTP)
Auth MethodUser/PassUser/PassKeys + PassKeys + Pass

💡 The Big Picture

If you are maintaining an old system that only has an FTP server, migrate from ftp to basic-ftp immediately. The promise-based API will save you hours of debugging, and basic-ftp supports modern TLS standards better.

However, for any new architecture, avoid FTP entirely. It is fragile and less secure. Use SSH.

  • Choose node-ssh if you are building deployment tools, CI/CD scripts, or any application that needs to run commands and move files. Its API is the most intuitive for general server management.
  • Choose ssh2-sftp-client if your application is a dedicated file synchronization service that moves large volumes of data and needs robust stream handling, without the need for running remote shell commands.

Final Thought: Security and maintainability should drive your choice. node-ssh offers the best balance of power and simplicity for most modern full-stack developers, while basic-ftp remains the only sensible choice if you are forced to interact with legacy FTP infrastructure.

How to Choose: basic-ftp vs ftp vs node-ssh vs ssh2-sftp-client

  • basic-ftp:

    Choose basic-ftp for new projects requiring FTP or FTPS support where modern async/await syntax is preferred. It is actively maintained, has zero dependencies, and offers a clean, promise-based API that simplifies error handling compared to older event-emitter libraries. Avoid it if you need SSH capabilities or complex stream manipulation not covered by its high-level methods.

  • ftp:

    Do NOT choose ftp for new projects; it is deprecated and no longer maintained. While it was once the standard for FTP in Node.js, its event-based API is harder to manage than modern promise-based alternatives, and it lacks security updates. If you encounter this in legacy code, plan a migration to basic-ftp immediately to ensure stability and security.

  • node-ssh:

    Choose node-ssh when you need a versatile tool for both executing remote shell commands and transferring files over SSH/SFTP. Its API is designed for developer happiness, offering simple methods like exec() and putFile() without requiring deep knowledge of the underlying SSH protocol. It is ideal for deployment scripts, CI/CD pipelines, and general server administration tasks where ease of use is paramount.

  • ssh2-sftp-client:

    Choose ssh2-sftp-client if your primary requirement is robust, high-performance file transfer (SFTP) rather than command execution. It provides a dedicated, promise-based interface specifically tuned for file operations like uploads, downloads, and directory listing, often offering finer control over stream handling than general-purpose SSH wrappers. It is the best fit for data-intensive applications where reliable file synchronization is the core requirement.

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.

The timeout applies to the server: a transfer fails if the server stops making progress for that long. It doesn't limit how long your own streams may take. A download piped into a slow destination, or an upload fed by a slow source, can hold up a transfer for as long as it needs to without running into a timeout. If you want to limit that as well, do it in your own code.

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. This is a potential vector for FTP bounce attacks, so by default this is set to false and the library will throw an error if a server tries to redirect transfers to a different host. Set this to true only if you are connecting to a server that legitimately requires it.

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.