diff --git a/aws_lambda_builders/__init__.py b/aws_lambda_builders/__init__.py
index bb83800ad..22867c8ce 100644
--- a/aws_lambda_builders/__init__.py
+++ b/aws_lambda_builders/__init__.py
@@ -1,5 +1,5 @@
 """
 AWS Lambda Builder Library
 """
-__version__ = "1.11.0"
+__version__ = "1.12.0"
 RPC_PROTOCOL_VERSION = "0.3"
diff --git a/aws_lambda_builders/__main__.py b/aws_lambda_builders/__main__.py
index bb7340c4c..2a0c3d887 100644
--- a/aws_lambda_builders/__main__.py
+++ b/aws_lambda_builders/__main__.py
@@ -129,6 +129,8 @@ def main():  # pylint: disable=too-many-statements
             dependencies_dir=params.get("dependencies_dir", None),
             combine_dependencies=params.get("combine_dependencies", True),
             architecture=params.get("architecture", X86_64),
+            is_building_layer=params.get("is_building_layer", False),
+            experimental_flags=params.get("experimental_flags", []),
         )
 
         # Return a success response
diff --git a/aws_lambda_builders/actions.py b/aws_lambda_builders/actions.py
index d9025a537..489e0b722 100644
--- a/aws_lambda_builders/actions.py
+++ b/aws_lambda_builders/actions.py
@@ -136,7 +136,7 @@ def execute(self):
             if os.path.isdir(dependencies_source):
                 copytree(dependencies_source, new_destination)
             else:
-                os.makedirs(os.path.dirname(dependencies_source), exist_ok=True)
+                os.makedirs(os.path.dirname(new_destination), exist_ok=True)
                 shutil.copy2(dependencies_source, new_destination)
 
 
@@ -162,6 +162,10 @@ def execute(self):
             dependencies_source = os.path.join(self.artifact_dir, name)
             new_destination = os.path.join(self.dest_dir, name)
 
+            # shutil.move can't create subfolders if this is the first file in that folder
+            if os.path.isfile(dependencies_source):
+                os.makedirs(os.path.dirname(new_destination), exist_ok=True)
+
             shutil.move(dependencies_source, new_destination)
 
 
diff --git a/aws_lambda_builders/workflows/nodejs_npm_esbuild/actions.py b/aws_lambda_builders/workflows/nodejs_npm_esbuild/actions.py
index ebcaf5227..8e14ebfc7 100644
--- a/aws_lambda_builders/workflows/nodejs_npm_esbuild/actions.py
+++ b/aws_lambda_builders/workflows/nodejs_npm_esbuild/actions.py
@@ -2,6 +2,8 @@
 Actions specific to the esbuild bundler
 """
 import logging
+from tempfile import NamedTemporaryFile
+
 from pathlib import Path
 
 from aws_lambda_builders.actions import BaseAction, Purpose, ActionFailedError
@@ -23,7 +25,16 @@ class EsbuildBundleAction(BaseAction):
 
     ENTRY_POINTS = "entry_points"
 
-    def __init__(self, scratch_dir, artifacts_dir, bundler_config, osutils, subprocess_esbuild):
+    def __init__(
+        self,
+        scratch_dir,
+        artifacts_dir,
+        bundler_config,
+        osutils,
+        subprocess_esbuild,
+        subprocess_nodejs=None,
+        skip_deps=False,
+    ):
         """
         :type scratch_dir: str
         :param scratch_dir: an existing (writable) directory for temporary files
@@ -35,8 +46,14 @@ def __init__(self, scratch_dir, artifacts_dir, bundler_config, osutils, subproce
         :type osutils: aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils
         :param osutils: An instance of OS Utilities for file manipulation
 
-        :type subprocess_esbuild: aws_lambda_builders.workflows.nodejs_npm.npm.SubprocessEsbuild
+        :type subprocess_esbuild: aws_lambda_builders.workflows.nodejs_npm_esbuild.esbuild.SubprocessEsbuild
         :param subprocess_esbuild: An instance of the Esbuild process wrapper
+
+        :type subprocess_nodejs: aws_lambda_builders.workflows.nodejs_npm_esbuild.node.SubprocessNodejs
+        :param subprocess_nodejs: An instance of the nodejs process wrapper
+
+        :type skip_deps: bool
+        :param skip_deps: if dependencies should be omitted from bundling
         """
         super(EsbuildBundleAction, self).__init__()
         self.scratch_dir = scratch_dir
@@ -44,6 +61,8 @@ def __init__(self, scratch_dir, artifacts_dir, bundler_config, osutils, subproce
         self.bundler_config = bundler_config
         self.osutils = osutils
         self.subprocess_esbuild = subprocess_esbuild
+        self.skip_deps = skip_deps
+        self.subprocess_nodejs = subprocess_nodejs
 
     def execute(self):
         """
@@ -81,11 +100,73 @@ def execute(self):
             args.append("--sourcemap")
         args.append("--target={}".format(target))
         args.append("--outdir={}".format(self.artifacts_dir))
+
+        if self.skip_deps:
+            LOG.info("Running custom esbuild using Node.js")
+            script = EsbuildBundleAction._get_node_esbuild_template(
+                explicit_entry_points, target, self.artifacts_dir, minify, sourcemap
+            )
+            self._run_external_esbuild_in_nodejs(script)
+            return
+
         try:
             self.subprocess_esbuild.run(args, cwd=self.scratch_dir)
         except EsbuildExecutionError as ex:
             raise ActionFailedError(str(ex))
 
+    def _run_external_esbuild_in_nodejs(self, script):
+        """
+        Run esbuild in a separate process through Node.js
+        Workaround for https://github.com/evanw/esbuild/issues/1958
+
+        :type script: str
+        :param script: Node.js script to execute
+
+        :raises lambda_builders.actions.ActionFailedError: when esbuild packaging fails
+        """
+        with NamedTemporaryFile(dir=self.scratch_dir, mode="w") as tmp:
+            tmp.write(script)
+            tmp.flush()
+            try:
+                self.subprocess_nodejs.run([tmp.name], cwd=self.scratch_dir)
+            except EsbuildExecutionError as ex:
+                raise ActionFailedError(str(ex))
+
+    @staticmethod
+    def _get_node_esbuild_template(entry_points, target, out_dir, minify, sourcemap):
+        """
+        Get the esbuild nodejs plugin template
+
+        :type entry_points: List[str]
+        :param entry_points: list of entry points
+
+        :type target: str
+        :param target: target version
+
+        :type out_dir: str
+        :param out_dir: output directory to bundle into
+
+        :type minify: bool
+        :param minify: if bundled code should be minified
+
+        :type sourcemap: bool
+        :param sourcemap: if esbuild should produce a sourcemap
+
+        :rtype: str
+        :return: formatted template
+        """
+        curr_dir = Path(__file__).resolve().parent
+        with open(str(Path(curr_dir, "esbuild-plugin.js.template")), "r") as f:
+            input_str = f.read()
+            result = input_str.format(
+                target=target,
+                minify="true" if minify else "false",
+                sourcemap="true" if sourcemap else "false",
+                out_dir=repr(out_dir),
+                entry_points=entry_points,
+            )
+        return result
+
     def _get_explicit_file_type(self, entry_point, entry_path):
         """
         Get an entry point with an explicit .ts or .js suffix.
@@ -112,3 +193,67 @@ def _get_explicit_file_type(self, entry_point, entry_path):
                 return entry_point + ext
 
         raise ActionFailedError("entry point {} does not exist".format(entry_path))
+
+
+class EsbuildCheckVersionAction(BaseAction):
+    """
+    A Lambda Builder Action that verifies that esbuild is a version supported by sam accelerate
+    """
+
+    NAME = "EsbuildCheckVersion"
+    DESCRIPTION = "Checking esbuild version"
+    PURPOSE = Purpose.COMPILE_SOURCE
+
+    MIN_VERSION = "0.14.13"
+
+    def __init__(self, scratch_dir, subprocess_esbuild):
+        """
+        :type scratch_dir: str
+        :param scratch_dir: temporary directory where esbuild is executed
+
+        :type subprocess_esbuild: aws_lambda_builders.workflows.nodejs_npm_esbuild.esbuild.SubprocessEsbuild
+        :param subprocess_esbuild: An instance of the Esbuild process wrapper
+        """
+        super().__init__()
+        self.scratch_dir = scratch_dir
+        self.subprocess_esbuild = subprocess_esbuild
+
+    def execute(self):
+        """
+        Runs the action.
+
+        :raises lambda_builders.actions.ActionFailedError: when esbuild version checking fails
+        """
+        args = ["--version"]
+
+        try:
+            version = self.subprocess_esbuild.run(args, cwd=self.scratch_dir)
+        except EsbuildExecutionError as ex:
+            raise ActionFailedError(str(ex))
+
+        LOG.debug("Found esbuild with version: %s", version)
+
+        try:
+            check_version = EsbuildCheckVersionAction._get_version_tuple(self.MIN_VERSION)
+            esbuild_version = EsbuildCheckVersionAction._get_version_tuple(version)
+
+            if esbuild_version < check_version:
+                raise ActionFailedError(
+                    f"Unsupported esbuild version. To use a dependency layer, the esbuild version must be at "
+                    f"least {self.MIN_VERSION}. Version found: {version}"
+                )
+        except (TypeError, ValueError) as ex:
+            raise ActionFailedError(f"Unable to parse esbuild version: {str(ex)}")
+
+    @staticmethod
+    def _get_version_tuple(version_string):
+        """
+        Get an integer tuple representation of the version for comparison
+
+        :type version_string: str
+        :param version_string: string containing the esbuild version
+
+        :rtype: tuple
+        :return: version tuple used for comparison
+        """
+        return tuple(map(int, version_string.split(".")))
diff --git a/aws_lambda_builders/workflows/nodejs_npm_esbuild/esbuild-plugin.js.template b/aws_lambda_builders/workflows/nodejs_npm_esbuild/esbuild-plugin.js.template
new file mode 100644
index 000000000..f3d2beb99
--- /dev/null
+++ b/aws_lambda_builders/workflows/nodejs_npm_esbuild/esbuild-plugin.js.template
@@ -0,0 +1,19 @@
+let skipBundleNodeModules = {{
+  name: 'make-all-packages-external',
+  setup(build) {{
+    let filter = /^[^.\/]|^\.[^.\/]|^\.\.[^\/]/ // Must not start with "/" or "./" or "../"
+    build.onResolve({{ filter }}, args => ({{ path: args.path, external: true }}))
+  }},
+}}
+
+require('esbuild').build({{
+  entryPoints: {entry_points},
+  bundle: true,
+  platform: 'node',
+  format: 'cjs',
+  target: '{target}',
+  sourcemap: {sourcemap},
+  outdir: {out_dir},
+  minify: {minify},
+  plugins: [skipBundleNodeModules],
+}}).catch(() => process.exit(1))
diff --git a/aws_lambda_builders/workflows/nodejs_npm_esbuild/node.py b/aws_lambda_builders/workflows/nodejs_npm_esbuild/node.py
new file mode 100644
index 000000000..6a50ea495
--- /dev/null
+++ b/aws_lambda_builders/workflows/nodejs_npm_esbuild/node.py
@@ -0,0 +1,104 @@
+"""
+Wrapper around calling nodejs through a subprocess.
+"""
+
+import logging
+
+from aws_lambda_builders.exceptions import LambdaBuilderError
+
+LOG = logging.getLogger(__name__)
+
+
+class NodejsExecutionError(LambdaBuilderError):
+
+    """
+    Exception raised in case nodejs execution fails.
+    It will pass on the standard error output from the Node.js console.
+    """
+
+    MESSAGE = "Nodejs Failed: {message}"
+
+
+class SubprocessNodejs(object):
+
+    """
+    Wrapper around the nodejs command line utility, making it
+    easy to consume execution results.
+    """
+
+    def __init__(self, osutils, executable_search_paths, which):
+        """
+        :type osutils: aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils
+        :param osutils: An instance of OS Utilities for file manipulation
+
+        :type executable_search_paths: list
+        :param executable_search_paths: List of paths to the node package binary utilities. This will
+            be used to find embedded Nodejs at runtime if present in the package
+
+        :type which: aws_lambda_builders.utils.which
+        :param which: Function to get paths which conform to the given mode on the PATH
+            with the prepended additional search paths
+        """
+        self.osutils = osutils
+        self.executable_search_paths = executable_search_paths
+        self.which = which
+
+    def nodejs_binary(self):
+        """
+        Finds the Nodejs binary at runtime.
+
+        The utility may be present as a package dependency of the Lambda project,
+        or in the global path. If there is one in the Lambda project, it should
+        be preferred over a global utility. The check has to be executed
+        at runtime, since nodejs dependencies will be installed by the workflow
+        using one of the previous actions.
+        """
+
+        LOG.debug("checking for nodejs in: %s", self.executable_search_paths)
+        binaries = self.which("node", executable_search_paths=self.executable_search_paths)
+        LOG.debug("potential nodejs binaries: %s", binaries)
+
+        if binaries:
+            return binaries[0]
+        else:
+            raise NodejsExecutionError(message="cannot find nodejs")
+
+    def run(self, args, cwd=None):
+
+        """
+        Runs the action.
+
+        :type args: list
+        :param args: Command line arguments to pass to Nodejs
+
+        :type cwd: str
+        :param cwd: Directory where to execute the command (defaults to current dir)
+
+        :rtype: str
+        :return: text of the standard output from the command
+
+        :raises aws_lambda_builders.workflows.nodejs_npm.npm.NodejsExecutionError:
+            when the command executes with a non-zero return code. The exception will
+            contain the text of the standard error output from the command.
+
+        :raises ValueError: if arguments are not provided, or not a list
+        """
+
+        if not isinstance(args, list):
+            raise ValueError("args must be a list")
+
+        if not args:
+            raise ValueError("requires at least one arg")
+
+        invoke_nodejs = [self.nodejs_binary()] + args
+
+        LOG.debug("executing Nodejs: %s", invoke_nodejs)
+
+        p = self.osutils.popen(invoke_nodejs, stdout=self.osutils.pipe, stderr=self.osutils.pipe, cwd=cwd)
+
+        out, err = p.communicate()
+
+        if p.returncode != 0:
+            raise NodejsExecutionError(message=err.decode("utf8").strip())
+
+        return out.decode("utf8").strip()
diff --git a/aws_lambda_builders/workflows/nodejs_npm_esbuild/workflow.py b/aws_lambda_builders/workflows/nodejs_npm_esbuild/workflow.py
index 280b8c14e..bb59c1545 100644
--- a/aws_lambda_builders/workflows/nodejs_npm_esbuild/workflow.py
+++ b/aws_lambda_builders/workflows/nodejs_npm_esbuild/workflow.py
@@ -4,15 +4,22 @@
 
 import logging
 import json
+from typing import List
 
 from aws_lambda_builders.workflow import BaseWorkflow, Capability
 from aws_lambda_builders.actions import (
     CopySourceAction,
+    CleanUpAction,
+    CopyDependenciesAction,
+    MoveDependenciesAction,
+    BaseAction,
 )
 from aws_lambda_builders.utils import which
 from .actions import (
     EsbuildBundleAction,
+    EsbuildCheckVersionAction,
 )
+from .node import SubprocessNodejs
 from .utils import is_experimental_esbuild_scope
 from .esbuild import SubprocessEsbuild, EsbuildExecutionError
 from ..nodejs_npm.actions import NodejsNpmCIAction, NodejsNpmInstallAction
@@ -66,7 +73,7 @@ def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, runtim
 
     def actions_with_bundler(
         self, source_dir, scratch_dir, artifacts_dir, bundler_config, osutils, subprocess_npm, subprocess_esbuild
-    ):
+    ) -> List[BaseAction]:
         """
         Generate a list of Nodejs build actions with a bundler
 
@@ -97,15 +104,89 @@ def actions_with_bundler(
         lockfile_path = osutils.joinpath(source_dir, "package-lock.json")
         shrinkwrap_path = osutils.joinpath(source_dir, "npm-shrinkwrap.json")
 
-        copy_action = CopySourceAction(source_dir, scratch_dir, excludes=self.EXCLUDED_FILES)
+        actions: List[BaseAction] = [
+            CopySourceAction(source_dir, scratch_dir, excludes=self.EXCLUDED_FILES + tuple(["node_modules"]))
+        ]
+
+        subprocess_node = SubprocessNodejs(osutils, self.executable_search_paths, which=which)
+
+        # Bundle dependencies separately in a dependency layer. We need to check the esbuild
+        # version here to ensure that it supports skipping dependency bundling
+        esbuild_no_deps = [
+            EsbuildCheckVersionAction(scratch_dir, subprocess_esbuild),
+            EsbuildBundleAction(
+                scratch_dir,
+                artifacts_dir,
+                bundler_config,
+                osutils,
+                subprocess_esbuild,
+                subprocess_node,
+                skip_deps=True,
+            ),
+        ]
+        esbuild_with_deps = EsbuildBundleAction(scratch_dir, artifacts_dir, bundler_config, osutils, subprocess_esbuild)
 
         if osutils.file_exists(lockfile_path) or osutils.file_exists(shrinkwrap_path):
             install_action = NodejsNpmCIAction(scratch_dir, subprocess_npm=subprocess_npm)
         else:
             install_action = NodejsNpmInstallAction(scratch_dir, subprocess_npm=subprocess_npm, is_production=False)
 
-        esbuild_action = EsbuildBundleAction(scratch_dir, artifacts_dir, bundler_config, osutils, subprocess_esbuild)
-        return [copy_action, install_action, esbuild_action]
+        if self.download_dependencies and not self.dependencies_dir:
+            return actions + [install_action, esbuild_with_deps]
+
+        return self._accelerate_workflow_actions(
+            source_dir, scratch_dir, actions, install_action, esbuild_with_deps, esbuild_no_deps
+        )
+
+    def _accelerate_workflow_actions(
+        self, source_dir, scratch_dir, actions, install_action, esbuild_with_deps, esbuild_no_deps
+    ):
+        """
+        Generate a list of Nodejs build actions for incremental build and auto dependency layer
+
+        :type source_dir: str
+        :param source_dir: an existing (readable) directory containing source files
+
+        :type scratch_dir: str
+        :param scratch_dir: an existing (writable) directory for temporary files
+
+        :type actions: List[BaseAction]
+        :param actions: List of existing actions
+
+        :type install_action: BaseAction
+        :param install_action: Installation action for npm
+
+        :type esbuild_with_deps: BaseAction
+        :param esbuild_with_deps: Standard esbuild action bundling source with deps
+
+        :type esbuild_no_deps: List[BaseAction]
+        :param esbuild_no_deps: esbuild action not including dependencies in the bundled artifacts
+
+        :rtype: list
+        :return: List of build actions to execute
+        """
+        if self.download_dependencies:
+            actions += [install_action, CleanUpAction(self.dependencies_dir)]
+            if self.combine_dependencies:
+                # Auto dependency layer disabled, first build
+                actions += [esbuild_with_deps, CopyDependenciesAction(source_dir, scratch_dir, self.dependencies_dir)]
+            else:
+                # Auto dependency layer enabled, first build
+                actions += esbuild_no_deps + [MoveDependenciesAction(source_dir, scratch_dir, self.dependencies_dir)]
+        else:
+            if self.dependencies_dir:
+                actions.append(CopySourceAction(self.dependencies_dir, scratch_dir))
+                if self.combine_dependencies:
+                    # Auto dependency layer disabled, subsequent builds
+                    actions += [esbuild_with_deps]
+                else:
+                    # Auto dependency layer enabled, subsequent builds
+                    actions += esbuild_no_deps
+            else:
+                # Invalid workflow, can't have no dependency dir and no installation
+                raise EsbuildExecutionError(message="Lambda Builders encountered and invalid workflow")
+
+        return actions
 
     def get_build_properties(self):
         """
diff --git a/tests/functional/test_cli.py b/tests/functional/test_cli.py
index dc7328977..3d3a64e5d 100644
--- a/tests/functional/test_cli.py
+++ b/tests/functional/test_cli.py
@@ -81,6 +81,8 @@ def test_run_hello_workflow_with_backcompat(self, flavor, protocol_version):
                 "dependencies_dir": "/ignored-dep",
                 "combine_dependencies": False,
                 "architecture": "x86_64",
+                "is_building_layer": False,
+                "experimental_flags": ["experimental"],
             },
         }
 
@@ -145,6 +147,8 @@ def test_run_hello_workflow_incompatible(self, flavor):
                     "download_dependencies": False,
                     "dependencies_dir": "/ignored-dep",
                     "combine_dependencies": False,
+                    "is_building_layer": False,
+                    "experimental_flags": ["experimental"],
                 },
             }
         )
diff --git a/tests/integration/test_actions.py b/tests/integration/test_actions.py
new file mode 100644
index 000000000..4e4f93a31
--- /dev/null
+++ b/tests/integration/test_actions.py
@@ -0,0 +1,59 @@
+import os
+from pathlib import Path
+import tempfile
+from unittest import TestCase
+from parameterized import parameterized
+
+
+from aws_lambda_builders.actions import CopyDependenciesAction, MoveDependenciesAction
+from aws_lambda_builders.utils import copytree
+
+
+class TestCopyDependenciesAction(TestCase):
+    @parameterized.expand(
+        [
+            ("single_file",),
+            ("multiple_files",),
+            ("empty_subfolders",),
+        ]
+    )
+    def test_copy_dependencies_action(self, source_folder):
+        curr_dir = Path(__file__).resolve().parent
+        test_folder = os.path.join(curr_dir, "testdata", source_folder)
+        with tempfile.TemporaryDirectory() as tmpdir:
+            empty_source = os.path.join(tmpdir, "empty_source")
+            target = os.path.join(tmpdir, "target")
+
+            os.mkdir(empty_source)
+
+            copy_dependencies_action = CopyDependenciesAction(empty_source, test_folder, target)
+            copy_dependencies_action.execute()
+
+            self.assertEqual(os.listdir(test_folder), os.listdir(target))
+
+
+class TestMoveDependenciesAction(TestCase):
+    @parameterized.expand(
+        [
+            ("single_file",),
+            ("multiple_files",),
+            ("empty_subfolders",),
+        ]
+    )
+    def test_move_dependencies_action(self, source_folder):
+        curr_dir = Path(__file__).resolve().parent
+        test_folder = os.path.join(curr_dir, "testdata", source_folder)
+        with tempfile.TemporaryDirectory() as tmpdir:
+            test_source = os.path.join(tmpdir, "test_source")
+            empty_source = os.path.join(tmpdir, "empty_source")
+            target = os.path.join(tmpdir, "target")
+
+            os.mkdir(test_source)
+            os.mkdir(empty_source)
+
+            copytree(test_folder, test_source)
+
+            move_dependencies_action = MoveDependenciesAction(empty_source, test_source, target)
+            move_dependencies_action.execute()
+
+            self.assertEqual(os.listdir(test_folder), os.listdir(target))
diff --git a/tests/integration/testdata/empty_subfolders/sub_folder/sub_folder/test_file.txt b/tests/integration/testdata/empty_subfolders/sub_folder/sub_folder/test_file.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/integration/testdata/multiple_files/sub_folder/test_file3.txt b/tests/integration/testdata/multiple_files/sub_folder/test_file3.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/integration/testdata/multiple_files/test_file.txt b/tests/integration/testdata/multiple_files/test_file.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/integration/testdata/multiple_files/test_file2.txt b/tests/integration/testdata/multiple_files/test_file2.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/integration/testdata/single_file/test_file.txt b/tests/integration/testdata/single_file/test_file.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/integration/workflows/nodejs_npm_esbuild/test_nodejs_npm_with_esbuild.py b/tests/integration/workflows/nodejs_npm_esbuild/test_nodejs_npm_with_esbuild.py
index f81f742a6..90c8c364d 100644
--- a/tests/integration/workflows/nodejs_npm_esbuild/test_nodejs_npm_with_esbuild.py
+++ b/tests/integration/workflows/nodejs_npm_esbuild/test_nodejs_npm_with_esbuild.py
@@ -20,6 +20,7 @@ class TestNodejsNpmWorkflowWithEsbuild(TestCase):
     def setUp(self):
         self.artifacts_dir = tempfile.mkdtemp()
         self.scratch_dir = tempfile.mkdtemp()
+        self.dependencies_dir = tempfile.mkdtemp()
 
         self.no_deps = os.path.join(self.TEST_DATA_FOLDER, "no-deps-esbuild")
 
@@ -185,3 +186,104 @@ def test_bundles_project_without_dependencies(self):
         expected_files = {"included.js.map", "included.js"}
         output_files = set(os.listdir(self.artifacts_dir))
         self.assertEqual(expected_files, output_files)
+
+    def test_builds_project_with_remote_dependencies_without_download_dependencies_with_dependencies_dir(self):
+        source_dir = os.path.join(self.TEST_DATA_FOLDER, "with-deps-no-node_modules")
+        options = {"entry_points": ["included.js"]}
+
+        osutils = OSUtils()
+        npm = SubprocessNpm(osutils)
+        esbuild_dir = os.path.join(self.TEST_DATA_FOLDER, "esbuild-binary")
+        npm.run(["ci"], cwd=esbuild_dir)
+        binpath = npm.run(["bin"], cwd=esbuild_dir)
+
+        self.builder.build(
+            source_dir,
+            self.artifacts_dir,
+            self.scratch_dir,
+            os.path.join(source_dir, "package.json"),
+            options=options,
+            runtime=self.runtime,
+            dependencies_dir=self.dependencies_dir,
+            download_dependencies=False,
+            experimental_flags=[EXPERIMENTAL_FLAG_ESBUILD],
+            executable_search_paths=[binpath],
+        )
+
+        expected_files = {"included.js.map", "included.js"}
+        output_files = set(os.listdir(self.artifacts_dir))
+        self.assertEqual(expected_files, output_files)
+
+    def test_builds_project_with_remote_dependencies_with_download_dependencies_and_dependencies_dir(self):
+        source_dir = os.path.join(self.TEST_DATA_FOLDER, "with-deps-no-node_modules")
+        options = {"entry_points": ["included.js"]}
+
+        self.builder.build(
+            source_dir,
+            self.artifacts_dir,
+            self.scratch_dir,
+            os.path.join(source_dir, "package.json"),
+            runtime=self.runtime,
+            options=options,
+            dependencies_dir=self.dependencies_dir,
+            download_dependencies=True,
+            experimental_flags=[EXPERIMENTAL_FLAG_ESBUILD],
+        )
+
+        expected_files = {"included.js.map", "included.js"}
+        output_files = set(os.listdir(self.artifacts_dir))
+        self.assertEqual(expected_files, output_files)
+
+        expected_modules = "minimal-request-promise"
+        output_modules = set(os.listdir(os.path.join(self.dependencies_dir, "node_modules")))
+        self.assertIn(expected_modules, output_modules)
+
+        expected_dependencies_files = {"node_modules"}
+        output_dependencies_files = set(os.listdir(os.path.join(self.dependencies_dir)))
+        self.assertNotIn(expected_dependencies_files, output_dependencies_files)
+
+    def test_builds_project_with_remote_dependencies_without_download_dependencies_without_dependencies_dir(self):
+        source_dir = os.path.join(self.TEST_DATA_FOLDER, "with-deps-no-node_modules")
+
+        with self.assertRaises(EsbuildExecutionError) as context:
+            self.builder.build(
+                source_dir,
+                self.artifacts_dir,
+                self.scratch_dir,
+                os.path.join(source_dir, "package.json"),
+                runtime=self.runtime,
+                dependencies_dir=None,
+                download_dependencies=False,
+                experimental_flags=[EXPERIMENTAL_FLAG_ESBUILD],
+            )
+
+        self.assertEqual(str(context.exception), "Esbuild Failed: Lambda Builders encountered and invalid workflow")
+
+    def test_builds_project_without_combine_dependencies(self):
+        source_dir = os.path.join(self.TEST_DATA_FOLDER, "with-deps-no-node_modules")
+        options = {"entry_points": ["included.js"]}
+
+        self.builder.build(
+            source_dir,
+            self.artifacts_dir,
+            self.scratch_dir,
+            os.path.join(source_dir, "package.json"),
+            runtime=self.runtime,
+            options=options,
+            dependencies_dir=self.dependencies_dir,
+            download_dependencies=True,
+            combine_dependencies=False,
+            experimental_flags=[EXPERIMENTAL_FLAG_ESBUILD],
+        )
+
+        expected_files = {"included.js.map", "included.js"}
+        output_files = set(os.listdir(self.artifacts_dir))
+        self.assertEqual(expected_files, output_files)
+
+        expected_modules = "minimal-request-promise"
+        output_modules = set(os.listdir(os.path.join(self.dependencies_dir, "node_modules")))
+        self.assertIn(expected_modules, output_modules)
+
+        expected_dependencies_files = {"node_modules"}
+        output_dependencies_files = set(os.listdir(os.path.join(self.dependencies_dir)))
+        self.assertNotIn(expected_dependencies_files, output_dependencies_files)
diff --git a/tests/integration/workflows/nodejs_npm_esbuild/testdata/with-deps-no-node_modules/excluded.js b/tests/integration/workflows/nodejs_npm_esbuild/testdata/with-deps-no-node_modules/excluded.js
new file mode 100644
index 000000000..8bf8be437
--- /dev/null
+++ b/tests/integration/workflows/nodejs_npm_esbuild/testdata/with-deps-no-node_modules/excluded.js
@@ -0,0 +1,2 @@
+//excluded
+const x = 1;
diff --git a/tests/integration/workflows/nodejs_npm_esbuild/testdata/with-deps-no-node_modules/included.js b/tests/integration/workflows/nodejs_npm_esbuild/testdata/with-deps-no-node_modules/included.js
new file mode 100644
index 000000000..39c9fd4ef
--- /dev/null
+++ b/tests/integration/workflows/nodejs_npm_esbuild/testdata/with-deps-no-node_modules/included.js
@@ -0,0 +1,2 @@
+//included
+const x = 5
diff --git a/tests/integration/workflows/nodejs_npm_esbuild/testdata/with-deps-no-node_modules/package.json b/tests/integration/workflows/nodejs_npm_esbuild/testdata/with-deps-no-node_modules/package.json
new file mode 100644
index 000000000..aec66dd1a
--- /dev/null
+++ b/tests/integration/workflows/nodejs_npm_esbuild/testdata/with-deps-no-node_modules/package.json
@@ -0,0 +1,14 @@
+{
+  "name": "with-deps-esbuild",
+  "version": "1.0.0",
+  "description": "",
+  "keywords": [],
+  "author": "",
+  "license": "APACHE2.0",
+  "dependencies": {
+    "minimal-request-promise": "^1.5.0"
+  },
+  "devDependencies": {
+    "esbuild": "0.14.13"
+  }
+}
diff --git a/tests/unit/workflows/nodejs_npm_esbuild/test_actions.py b/tests/unit/workflows/nodejs_npm_esbuild/test_actions.py
index a67edaf95..9150249ca 100644
--- a/tests/unit/workflows/nodejs_npm_esbuild/test_actions.py
+++ b/tests/unit/workflows/nodejs_npm_esbuild/test_actions.py
@@ -1,17 +1,23 @@
+import tempfile
+from pathlib import Path
 from unittest import TestCase
+from unittest.mock import Mock
+
 from mock import patch
 from parameterized import parameterized
 
 from aws_lambda_builders.actions import ActionFailedError
-from aws_lambda_builders.workflows.nodejs_npm_esbuild.actions import EsbuildBundleAction
+from aws_lambda_builders.workflows.nodejs_npm_esbuild.actions import EsbuildBundleAction, EsbuildCheckVersionAction
 
 
 class TestEsbuildBundleAction(TestCase):
     @patch("aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils")
     @patch("aws_lambda_builders.workflows.nodejs_npm_esbuild.esbuild.SubprocessEsbuild")
-    def setUp(self, OSUtilMock, SubprocessEsbuildMock):
+    @patch("aws_lambda_builders.workflows.nodejs_npm_esbuild.node.SubprocessNodejs")
+    def setUp(self, OSUtilMock, SubprocessEsbuildMock, SubprocessNodejsMock):
         self.osutils = OSUtilMock.return_value
         self.subprocess_esbuild = SubprocessEsbuildMock.return_value
+        self.subprocess_nodejs = SubprocessNodejsMock.return_value
         self.osutils.joinpath.side_effect = lambda a, b: "{}/{}".format(a, b)
         self.osutils.file_exists.side_effect = [True, True]
 
@@ -173,6 +179,43 @@ def test_includes_multiple_entry_points_if_requested(self):
             cwd="source",
         )
 
+    def test_runs_node_subprocess_if_deps_skipped(self):
+        action = EsbuildBundleAction(
+            tempfile.mkdtemp(),
+            "artifacts",
+            {"entry_points": ["app.ts"]},
+            self.osutils,
+            self.subprocess_esbuild,
+            self.subprocess_nodejs,
+            True,
+        )
+        action.execute()
+        self.subprocess_nodejs.run.assert_called()
+
+    def test_reads_nodejs_bundle_template_file(self):
+        template = EsbuildBundleAction._get_node_esbuild_template(["app.ts"], "es2020", "outdir", False, True)
+        expected_template = """let skipBundleNodeModules = {
+  name: 'make-all-packages-external',
+  setup(build) {
+    let filter = /^[^.\/]|^\.[^.\/]|^\.\.[^\/]/ // Must not start with "/" or "./" or "../"
+    build.onResolve({ filter }, args => ({ path: args.path, external: true }))
+  },
+}
+
+require('esbuild').build({
+  entryPoints: ['app.ts'],
+  bundle: true,
+  platform: 'node',
+  format: 'cjs',
+  target: 'es2020',
+  sourcemap: true,
+  outdir: 'outdir',
+  minify: false,
+  plugins: [skipBundleNodeModules],
+}).catch(() => process.exit(1))
+"""
+        self.assertEqual(template, expected_template)
+
 
 class TestImplicitFileTypeResolution(TestCase):
     @patch("aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils")
@@ -211,3 +254,39 @@ def test_throws_exception_entry_point_not_found(self, file_exists, entry_point):
         with self.assertRaises(ActionFailedError) as context:
             self.action._get_explicit_file_type(entry_point, "invalid")
         self.assertEqual(str(context.exception), "entry point invalid does not exist")
+
+
+class TestEsbuildVersionCheckerAction(TestCase):
+    @parameterized.expand(["0.14.0", "0.0.0", "0.14.12"])
+    def test_outdated_esbuild_versions(self, version):
+        subprocess_esbuild = Mock()
+        subprocess_esbuild.run.return_value = version
+        action = EsbuildCheckVersionAction("scratch", subprocess_esbuild)
+        with self.assertRaises(ActionFailedError) as content:
+            action.execute()
+        self.assertEqual(
+            str(content.exception),
+            f"Unsupported esbuild version. To use a dependency layer, the esbuild version "
+            f"must be at least 0.14.13. Version found: {version}",
+        )
+
+    @parameterized.expand(["a.0.0", "a.b.c"])
+    def test_invalid_esbuild_versions(self, version):
+        subprocess_esbuild = Mock()
+        subprocess_esbuild.run.return_value = version
+        action = EsbuildCheckVersionAction("scratch", subprocess_esbuild)
+        with self.assertRaises(ActionFailedError) as content:
+            action.execute()
+        self.assertEqual(
+            str(content.exception), "Unable to parse esbuild version: invalid literal for int() with base 10: 'a'"
+        )
+
+    @parameterized.expand(["0.14.13", "1.0.0", "10.0.10"])
+    def test_valid_esbuild_versions(self, version):
+        subprocess_esbuild = Mock()
+        subprocess_esbuild.run.return_value = version
+        action = EsbuildCheckVersionAction("scratch", subprocess_esbuild)
+        try:
+            action.execute()
+        except ActionFailedError:
+            self.fail("Encountered an unexpected exception.")
diff --git a/tests/unit/workflows/nodejs_npm_esbuild/test_node.py b/tests/unit/workflows/nodejs_npm_esbuild/test_node.py
new file mode 100644
index 000000000..d9a7c89c2
--- /dev/null
+++ b/tests/unit/workflows/nodejs_npm_esbuild/test_node.py
@@ -0,0 +1,77 @@
+from unittest import TestCase
+from mock import patch
+
+from aws_lambda_builders.workflows.nodejs_npm_esbuild.node import SubprocessNodejs, NodejsExecutionError
+
+
+class FakePopen:
+    def __init__(self, out=b"out", err=b"err", retcode=0):
+        self.out = out
+        self.err = err
+        self.returncode = retcode
+
+    def communicate(self):
+        return self.out, self.err
+
+
+class TestSubprocessNodejs(TestCase):
+    @patch("aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils")
+    def setUp(self, OSUtilMock):
+        self.osutils = OSUtilMock.return_value
+        self.osutils.pipe = "PIPE"
+        self.popen = FakePopen()
+        self.osutils.popen.side_effect = [self.popen]
+
+        which = lambda cmd, executable_search_paths: ["{}/{}".format(executable_search_paths[0], cmd)]
+
+        self.under_test = SubprocessNodejs(self.osutils, ["/a/b", "/c/d"], which)
+
+    def test_run_executes_binary_found_in_exec_paths(self):
+
+        self.under_test.run(["arg-a", "arg-b"])
+
+        self.osutils.popen.assert_called_with(["/a/b/node", "arg-a", "arg-b"], cwd=None, stderr="PIPE", stdout="PIPE")
+
+    def test_uses_cwd_if_supplied(self):
+        self.under_test.run(["arg-a", "arg-b"], cwd="/a/cwd")
+
+        self.osutils.popen.assert_called_with(
+            ["/a/b/node", "arg-a", "arg-b"], cwd="/a/cwd", stderr="PIPE", stdout="PIPE"
+        )
+
+    def test_returns_popen_out_decoded_if_retcode_is_0(self):
+        self.popen.out = b"some encoded text\n\n"
+
+        result = self.under_test.run(["pack"])
+
+        self.assertEqual(result, "some encoded text")
+
+    def test_raises_NodejsExecutionError_with_err_text_if_retcode_is_not_0(self):
+        self.popen.returncode = 1
+        self.popen.err = b"some error text\n\n"
+
+        with self.assertRaises(NodejsExecutionError) as raised:
+            self.under_test.run(["pack"])
+
+        self.assertEqual(raised.exception.args[0], "Nodejs Failed: some error text")
+
+    def test_raises_NodejsExecutionError_if_which_returns_no_results(self):
+
+        which = lambda cmd, executable_search_paths: []
+        self.under_test = SubprocessNodejs(self.osutils, ["/a/b", "/c/d"], which)
+        with self.assertRaises(NodejsExecutionError) as raised:
+            self.under_test.run(["pack"])
+
+        self.assertEqual(raised.exception.args[0], "Nodejs Failed: cannot find nodejs")
+
+    def test_raises_ValueError_if_args_not_a_list(self):
+        with self.assertRaises(ValueError) as raised:
+            self.under_test.run(("pack"))
+
+        self.assertEqual(raised.exception.args[0], "args must be a list")
+
+    def test_raises_ValueError_if_args_empty(self):
+        with self.assertRaises(ValueError) as raised:
+            self.under_test.run([])
+
+        self.assertEqual(raised.exception.args[0], "requires at least one arg")
diff --git a/tests/unit/workflows/nodejs_npm_esbuild/test_workflow.py b/tests/unit/workflows/nodejs_npm_esbuild/test_workflow.py
index 11bb8afab..ccaaa4822 100644
--- a/tests/unit/workflows/nodejs_npm_esbuild/test_workflow.py
+++ b/tests/unit/workflows/nodejs_npm_esbuild/test_workflow.py
@@ -1,12 +1,11 @@
 from unittest import TestCase
 from mock import patch, call
 
-from aws_lambda_builders.actions import CopySourceAction
-from aws_lambda_builders.exceptions import WorkflowFailedError
+from aws_lambda_builders.actions import CopySourceAction, CleanUpAction, CopyDependenciesAction, MoveDependenciesAction
 from aws_lambda_builders.architecture import ARM64
 from aws_lambda_builders.workflows.nodejs_npm.actions import NodejsNpmInstallAction, NodejsNpmCIAction
 from aws_lambda_builders.workflows.nodejs_npm_esbuild import NodejsNpmEsbuildWorkflow
-from aws_lambda_builders.workflows.nodejs_npm_esbuild.actions import EsbuildBundleAction
+from aws_lambda_builders.workflows.nodejs_npm_esbuild.actions import EsbuildBundleAction, EsbuildCheckVersionAction
 from aws_lambda_builders.workflows.nodejs_npm_esbuild.esbuild import SubprocessEsbuild
 from aws_lambda_builders.workflows.nodejs_npm_esbuild.utils import EXPERIMENTAL_FLAG_ESBUILD
 
@@ -167,3 +166,106 @@ def test_must_validate_architecture(self):
 
         self.assertEqual(workflow.architecture, "x86_64")
         self.assertEqual(workflow_with_arm.architecture, "arm64")
+
+    def test_workflow_sets_up_esbuild_actions_with_download_dependencies_without_dependencies_dir(self):
+        self.osutils.file_exists.return_value = True
+
+        workflow = NodejsNpmEsbuildWorkflow(
+            "source",
+            "artifacts",
+            "scratch_dir",
+            "manifest",
+            osutils=self.osutils,
+            experimental_flags=[EXPERIMENTAL_FLAG_ESBUILD],
+        )
+
+        self.assertEqual(len(workflow.actions), 3)
+        self.assertIsInstance(workflow.actions[0], CopySourceAction)
+        self.assertIsInstance(workflow.actions[1], NodejsNpmCIAction)
+        self.assertIsInstance(workflow.actions[2], EsbuildBundleAction)
+
+    def test_workflow_sets_up_esbuild_actions_without_download_dependencies_with_dependencies_dir_combine_deps(self):
+        self.osutils.file_exists.return_value = True
+
+        workflow = NodejsNpmEsbuildWorkflow(
+            "source",
+            "artifacts",
+            "scratch_dir",
+            "manifest",
+            dependencies_dir="dep",
+            download_dependencies=False,
+            combine_dependencies=True,
+            osutils=self.osutils,
+            experimental_flags=[EXPERIMENTAL_FLAG_ESBUILD],
+        )
+
+        self.assertEqual(len(workflow.actions), 3)
+        self.assertIsInstance(workflow.actions[0], CopySourceAction)
+        self.assertIsInstance(workflow.actions[1], CopySourceAction)
+        self.assertIsInstance(workflow.actions[2], EsbuildBundleAction)
+
+    def test_workflow_sets_up_esbuild_actions_without_download_dependencies_with_dependencies_dir_no_combine_deps(self):
+        self.osutils.file_exists.return_value = True
+
+        workflow = NodejsNpmEsbuildWorkflow(
+            "source",
+            "artifacts",
+            "scratch_dir",
+            "manifest",
+            dependencies_dir="dep",
+            download_dependencies=False,
+            combine_dependencies=False,
+            osutils=self.osutils,
+            experimental_flags=[EXPERIMENTAL_FLAG_ESBUILD],
+        )
+
+        self.assertEqual(len(workflow.actions), 4)
+        self.assertIsInstance(workflow.actions[0], CopySourceAction)
+        self.assertIsInstance(workflow.actions[1], CopySourceAction)
+        self.assertIsInstance(workflow.actions[2], EsbuildCheckVersionAction)
+        self.assertIsInstance(workflow.actions[3], EsbuildBundleAction)
+
+    def test_workflow_sets_up_esbuild_actions_with_download_dependencies_and_dependencies_dir(self):
+
+        self.osutils.file_exists.return_value = True
+
+        workflow = NodejsNpmEsbuildWorkflow(
+            "source",
+            "artifacts",
+            "scratch_dir",
+            "manifest",
+            dependencies_dir="dep",
+            download_dependencies=True,
+            osutils=self.osutils,
+            experimental_flags=[EXPERIMENTAL_FLAG_ESBUILD],
+        )
+
+        self.assertEqual(len(workflow.actions), 5)
+
+        self.assertIsInstance(workflow.actions[0], CopySourceAction)
+        self.assertIsInstance(workflow.actions[1], NodejsNpmCIAction)
+        self.assertIsInstance(workflow.actions[2], CleanUpAction)
+        self.assertIsInstance(workflow.actions[3], EsbuildBundleAction)
+        self.assertIsInstance(workflow.actions[4], CopyDependenciesAction)
+
+    def test_workflow_sets_up_esbuild_actions_with_download_dependencies_and_dependencies_dir_no_combine_deps(self):
+        workflow = NodejsNpmEsbuildWorkflow(
+            "source",
+            "artifacts",
+            "scratch_dir",
+            "manifest",
+            dependencies_dir="dep",
+            download_dependencies=True,
+            combine_dependencies=False,
+            osutils=self.osutils,
+            experimental_flags=[EXPERIMENTAL_FLAG_ESBUILD],
+        )
+
+        self.assertEqual(len(workflow.actions), 6)
+
+        self.assertIsInstance(workflow.actions[0], CopySourceAction)
+        self.assertIsInstance(workflow.actions[1], NodejsNpmCIAction)
+        self.assertIsInstance(workflow.actions[2], CleanUpAction)
+        self.assertIsInstance(workflow.actions[3], EsbuildCheckVersionAction)
+        self.assertIsInstance(workflow.actions[4], EsbuildBundleAction)
+        self.assertIsInstance(workflow.actions[5], MoveDependenciesAction)