diff --git a/packages/create-llama/e2e/basic.spec.ts b/packages/create-llama/e2e/basic.spec.ts
index 36b8d793f6687fa21e826900eea365ac0b2cb815..dbaeb4b9b718f5770e58b919ec1ef44b6a602bb3 100644
--- a/packages/create-llama/e2e/basic.spec.ts
+++ b/packages/create-llama/e2e/basic.spec.ts
@@ -1,6 +1,6 @@
 /* eslint-disable turbo/no-undeclared-env-vars */
 import { expect, test } from "@playwright/test";
-import { exec } from "child_process";
+import { ChildProcess, exec } from "child_process";
 import { execSync } from "node:child_process";
 import crypto from "node:crypto";
 import { mkdir } from "node:fs/promises";
@@ -21,109 +21,149 @@ test.beforeEach(async () => {
   await mkdir(cwd, { recursive: true });
 });
 
-const templateTypes: TemplateType[] = ["streaming", "simple"];
-const templateFrameworks: TemplateFramework[] = ["nextjs", "express"];
-const templateEngines: TemplateEngine[] = ["simple", "context"];
-const templateUIs: TemplateUI[] = ["shadcn", "html"];
+type AppType = "--frontend" | "--no-frontend" | "";
+
+const templateTypes: TemplateType[] = ["streaming"];
+const templateFrameworks: TemplateFramework[] = ["express"];
+const templateEngines: TemplateEngine[] = ["simple"];
+const templateUIs: TemplateUI[] = ["html"];
+const MODEL = "gpt-3.5-turbo";
 
 for (const templateType of templateTypes) {
   for (const templateFramework of templateFrameworks) {
     for (const templateEngine of templateEngines) {
       for (const templateUI of templateUIs) {
-        const shouldGenerateFrontendEnum =
+        const appType: AppType =
           templateFramework === "express" || templateFramework === "fastapi"
-            ? ["--frontend", "--no-frontend"]
-            : [""];
-        for (const shouldGenerateFrontend of shouldGenerateFrontendEnum) {
-          if (templateEngine === "context") {
-            // we don't test context templates because it needs OPEN_AI_KEY
-            continue;
-          }
-          test(`try create-llama ${templateType} ${templateFramework} ${templateEngine} ${templateUI} ${shouldGenerateFrontend}`, async ({
-            page,
-          }) => {
-            const createLlama = fileURLToPath(
-              new URL("../dist/index.js", import.meta.url),
-            );
+            ? "--frontend"
+            : "";
+        if (templateEngine === "context") {
+          // we don't test context templates because it needs OPEN_AI_KEY
+          continue;
+        }
+        test(`try create-llama ${templateType} ${templateFramework} ${templateEngine} ${templateUI} ${appType}`, async ({
+          page,
+        }) => {
+          const name = runCreateLlama(
+            templateType,
+            templateFramework,
+            templateEngine,
+            templateUI,
+            appType,
+          );
 
-            const name = [
-              templateType,
-              templateFramework,
-              templateEngine,
-              templateUI,
-              shouldGenerateFrontend,
-            ].join("-");
-            const command = [
-              "node",
-              createLlama,
-              name,
-              "--template",
-              templateType,
-              "--framework",
-              templateFramework,
-              "--engine",
-              templateEngine,
-              "--ui",
-              templateUI,
-              "--open-ai-key",
-              process.env.OPEN_AI_KEY || "",
-              shouldGenerateFrontend,
-              "--eslint",
-            ].join(" ");
-            console.log(`running command '${command}' in ${cwd}`);
-            execSync(command, {
-              stdio: "inherit",
-              cwd,
-            });
+          const port = Math.floor(Math.random() * 10000) + 10000;
+          const cps = await runApp(name, appType, port);
+
+          // test frontend
+          await page.goto(`http://localhost:${port}`);
+          await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
+          // TODO: test backend using curl (would need OpenAI key)
+          // clean processes
+          cps.forEach((cp) => cp.kill());
+        });
+      }
+    }
+  }
+}
 
-            const port = Math.floor(Math.random() * 10000) + 10000;
+async function runApp(
+  name: string,
+  appType: AppType,
+  port: number,
+): Promise<ChildProcess[]> {
+  const cps = [];
 
-            if (
-              shouldGenerateFrontend === "--frontend" &&
-              templateFramework === "express"
-            ) {
-              execSync("npm install", {
-                stdio: "inherit",
-                cwd: `${cwd}/${name}/frontend`,
-              });
-              execSync("npm install", {
-                stdio: "inherit",
-                cwd: `${cwd}/${name}/backend`,
-              });
-            } else {
-              execSync("npm install", {
-                stdio: "inherit",
-                cwd: `${cwd}/${name}`,
-              });
-            }
+  try {
+    switch (appType) {
+      case "--no-frontend":
+        cps.push(
+          await createProcess("npm run dev", `${cwd}/${name}/backend`, port),
+        );
+        break;
+      case "--frontend":
+        cps.push(
+          await createProcess(
+            "npm run dev",
+            `${cwd}/${name}/backend`,
+            port + 1,
+          ),
+        );
+        cps.push(
+          await createProcess("npm run dev", `${cwd}/${name}/frontend`, port),
+        );
+        break;
+      default:
+        cps.push(await createProcess("npm run dev", `${cwd}/${name}`, port));
+        break;
+    }
+  } catch (e) {
+    cps.forEach((cp) => cp.kill());
+    throw e;
+  }
+  return cps;
+}
 
-            if (shouldGenerateFrontend === "--no-frontend") {
-              return;
-            }
+async function createProcess(command: string, cwd: string, port: number) {
+  const cp = exec(command, {
+    cwd,
+    env: {
+      ...process.env,
+      PORT: `${port}`,
+    },
+  });
+  if (!cp) throw new Error(`Can't start process ${command} in ${cwd}`);
 
-            const cp = exec("npm run dev", {
-              cwd:
-                shouldGenerateFrontend === "--frontend"
-                  ? `${cwd}/${name}/frontend`
-                  : `${cwd}/${name}`,
-              env: {
-                ...process.env,
-                PORT: `${port}`,
-              },
-            });
+  await waitPort({
+    host: "localhost",
+    port,
+    timeout: 1000 * 60,
+  });
+  return cp;
+}
 
-            await waitPort({
-              host: "localhost",
-              port,
-              timeout: 1000 * 60,
-            });
+function runCreateLlama(
+  templateType: string,
+  templateFramework: string,
+  templateEngine: string,
+  templateUI: string,
+  appType: AppType,
+) {
+  const createLlama = fileURLToPath(
+    new URL("../dist/index.js", import.meta.url),
+  );
 
-            await page.goto(`http://localhost:${port}`);
-            await expect(page.getByText("Built by LlamaIndex")).toBeVisible();
-            cp.kill();
-          });
-        }
-      }
-    }
-  }
+  const name = [
+    templateType,
+    templateFramework,
+    templateEngine,
+    templateUI,
+    appType,
+  ].join("-");
+  const command = [
+    "node",
+    createLlama,
+    name,
+    "--template",
+    templateType,
+    "--framework",
+    templateFramework,
+    "--engine",
+    templateEngine,
+    "--ui",
+    templateUI,
+    "--model",
+    MODEL,
+    "--open-ai-key",
+    "testKey", // TODO: pass in OPEN_AI_KEY from CI env if needed for tests
+    appType,
+    "--eslint",
+    "--use-npm",
+  ].join(" ");
+  console.log(`running command '${command}' in ${cwd}`);
+  execSync(command, {
+    stdio: "inherit",
+    cwd,
+  });
+  return name;
 }
diff --git a/packages/create-llama/package.json b/packages/create-llama/package.json
index f0079ecbc156fd6175317c46de6bf7d96bfc1c23..3d77e5f2a539c10e79df5bc7c0ba14e00ebf5d17 100644
--- a/packages/create-llama/package.json
+++ b/packages/create-llama/package.json
@@ -24,7 +24,7 @@
     "dev": "ncc build ./index.ts -w -o dist/",
     "build": "ncc build ./index.ts -o ./dist/ --minify --no-cache --no-source-map-register",
     "lint": "eslint . --ignore-pattern dist",
-    "e2e": "playwright test",
+    "e2e": "playwright test --reporter=list",
     "prepublishOnly": "cd ../../ && turbo run build"
   },
   "devDependencies": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 079de736ae46d5bb6af79677bb4773e12547620a..33f5341147c4bc71214789168b9adbd9de3126b5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -240,6 +240,9 @@ importers:
 
   packages/create-llama:
     devDependencies:
+      '@playwright/test':
+        specifier: ^1.40.0
+        version: 1.40.0
       '@types/async-retry':
         specifier: 1.4.2
         version: 1.4.2
@@ -306,6 +309,9 @@ importers:
       validate-npm-package-name:
         specifier: 3.0.0
         version: 3.0.0
+      wait-port:
+        specifier: ^1.1.0
+        version: 1.1.0
 
   packages/eslint-config-custom:
     dependencies:
@@ -3903,6 +3909,14 @@ packages:
       tslib: 2.6.1
     dev: false
 
+  /@playwright/test@1.40.0:
+    resolution: {integrity: sha512-PdW+kn4eV99iP5gxWNSDQCbhMaDVej+RXL5xr6t04nbKLCBwYtA046t7ofoczHOm8u6c+45hpDKQVZqtqwkeQg==}
+    engines: {node: '>=16'}
+    hasBin: true
+    dependencies:
+      playwright: 1.40.0
+    dev: true
+
   /@polka/url@1.0.0-next.23:
     resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==}
     dev: false
@@ -6326,6 +6340,11 @@ packages:
     engines: {node: '>= 12'}
     dev: false
 
+  /commander@9.5.0:
+    resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
+    engines: {node: ^12.20.0 || >=14}
+    dev: true
+
   /commondir@1.0.1:
     resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
     dev: false
@@ -8559,6 +8578,14 @@ packages:
   /fs.realpath@1.0.0:
     resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
 
+  /fsevents@2.3.2:
+    resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+    os: [darwin]
+    requiresBuild: true
+    dev: true
+    optional: true
+
   /fsevents@2.3.3:
     resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -12147,6 +12174,22 @@ packages:
     resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==}
     dev: false
 
+  /playwright-core@1.40.0:
+    resolution: {integrity: sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==}
+    engines: {node: '>=16'}
+    hasBin: true
+    dev: true
+
+  /playwright@1.40.0:
+    resolution: {integrity: sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==}
+    engines: {node: '>=16'}
+    hasBin: true
+    dependencies:
+      playwright-core: 1.40.0
+    optionalDependencies:
+      fsevents: 2.3.2
+    dev: true
+
   /portkey-ai@0.1.16:
     resolution: {integrity: sha512-EY4FRp6PZSD75Q1o1qc08DfPNTG9FnkUPN3Z1/lEvaq9iFpSO5UekcagUZaKSVhao311qjBjns+kF0rS9ht7iA==}
     dependencies:
@@ -15655,6 +15698,18 @@ packages:
       - debug
     dev: false
 
+  /wait-port@1.1.0:
+    resolution: {integrity: sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==}
+    engines: {node: '>=10'}
+    hasBin: true
+    dependencies:
+      chalk: 4.1.2
+      commander: 9.5.0
+      debug: 4.3.4
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
   /walker@1.0.8:
     resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
     dependencies: