Skip to content

[APPS] Add vite dev server middleware for backend functions#291

Open
sdkennedy2 wants to merge 11 commits intosdkennedy2/apps-vite-backend-buildfrom
sdkennedy2/apps-vite-dev-server
Open

[APPS] Add vite dev server middleware for backend functions#291
sdkennedy2 wants to merge 11 commits intosdkennedy2/apps-vite-backend-buildfrom
sdkennedy2/apps-vite-dev-server

Conversation

@sdkennedy2
Copy link
Copy Markdown
Collaborator

@sdkennedy2 sdkennedy2 commented Mar 17, 2026

Motivation

High Code Apps backend functions need a local development experience. Currently, backend functions are only built at production build time — there's no way to invoke them during local development with hot reload.

Changes

Adds a Vite dev server middleware that intercepts requests to backend function endpoints, bundles the target function on-the-fly using Vite's build API, executes it, and returns the result. This gives developers a local dev loop for backend functions without needing a full production build.

Key pieces:

  • dev-server.ts — Express-style middleware that handles /execute-action requests. On each invocation it bundles the requested function using Vite's in-memory build, evaluates the bundle, and returns the result. Includes long-poll support for streaming responses.
  • build-config.ts — Extracted shared Vite build config (getBaseBackendBuildConfig) used by both the production multi-entry build and the dev single-function build, eliminating duplication of config options (resolve conditions, output format, virtual module plugin, etc.).
  • virtual-entry.ts — Added generateDevVirtualEntryContent for the dev build's virtual entry point, alongside the existing production entry generator.
  • Dev server validates requests and returns proper HTTP status codes (400 for bad requests, 404 for unknown functions, 403 for missing credentials, 500 for build/runtime errors).
  • Uses doRequest from @dd/core/helpers/request for outbound calls (consistency with codebase conventions, retry support).

QA Instructions

  1. Run yarn dev in a project with backend functions
  2. Trigger a backend function invocation from the frontend
  3. Verify the function executes and returns results correctly
  4. Verify hot reload — change the function source and re-invoke without restarting the dev server

Blast Radius

  • Only affects the Vite plugin for apps with backend functions during local development
  • No changes to production build output
  • Gated by the apps plugin — only active when apps configuration is present

Copy link
Copy Markdown
Collaborator Author

sdkennedy2 commented Mar 17, 2026

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@sdkennedy2 sdkennedy2 changed the title [APPS] Add vite dev server middleware for backend functions using host bundler [APPS] Add vite dev server middleware for backend functions Mar 17, 2026
@sdkennedy2 sdkennedy2 requested a review from sarenji March 17, 2026 14:49
@sdkennedy2 sdkennedy2 marked this pull request as ready for review March 17, 2026 14:50
@sdkennedy2 sdkennedy2 requested a review from yoannmoinet as a code owner March 17, 2026 14:50
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-vite-dev-server branch from a1630ef to 4f85ed9 Compare March 17, 2026 18:02
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-backend-functions-upload-v2 branch from 264286b to 2f9f167 Compare March 17, 2026 18:02
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-vite-dev-server branch from 7e20d87 to e465412 Compare March 18, 2026 15:53
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-backend-functions-upload-v2 branch from 2f9f167 to d682d52 Compare March 18, 2026 15:53
Base automatically changed from sdkennedy2/apps-backend-functions-upload-v2 to master March 19, 2026 15:43
Copy link
Copy Markdown
Contributor

@sarenji sarenji left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial thoughts on this

@sdkennedy2 sdkennedy2 changed the base branch from master to graphite-base/291 March 20, 2026 20:38
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-vite-dev-server branch from 95203d8 to a0932f3 Compare March 20, 2026 20:38
@sdkennedy2 sdkennedy2 changed the base branch from graphite-base/291 to sdkennedy2/apps-vite-backend-build March 20, 2026 20:52
Copy link
Copy Markdown
Member

@yoannmoinet yoannmoinet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should check in the other parts of the code for hints on how to use what's available to you in the repo.


describe('Dev Server Middleware', () => {
beforeEach(() => {
jest.clearAllMocks();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this is unnecessary as it is part of the Jest configuration.

Comment on lines +154 to +155
// Wait for async handler to complete.
await new Promise((resolve) => setTimeout(resolve, 100));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, and for all the followings as well.
You should not be doing this.

],
});

const output = (Array.isArray(result) ? result[0] : result) as RollupOutput;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need the as RollupOutput part here?
This should be typed from viteBuild, which should not have a basic Function type.

@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-vite-dev-server branch 2 times, most recently from 807bbdf to c0be60c Compare March 26, 2026 20:55
@datadog-prod-us1-6
Copy link
Copy Markdown

datadog-prod-us1-6 bot commented Mar 26, 2026

✅ Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 81b50d1 | Docs | Datadog PR Page | Was this helpful? React with 👍/👎 or give us feedback!

@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-vite-dev-server branch 5 times, most recently from 146a8f6 to 9c96053 Compare March 30, 2026 15:09
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-vite-backend-build branch from d2b2a1b to 03c5f9c Compare March 30, 2026 15:09
sdkennedy2 and others added 6 commits March 30, 2026 14:52
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…try bug

- Replace raw fetch calls with doRequest from @dd/core/helpers/request
  for retry support and consistency with codebase conventions.
- Replace flaky setTimeout waits in tests with deterministic res.done promise.
- Replace as-any casts with as-unknown-as-Type for proper type narrowing.
- Fix generateDevVirtualEntryContent calling undefined generateMainBody
  by inlining the main body generation (matching production entry pattern).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move common build options (configFile, logLevel, minify, target,
treeshake, preserveEntrySignatures, onwarn, resolve, output format,
and virtual module plugin) into a shared getBaseBackendBuildConfig()
helper. Each caller now only specifies what differs: production writes
multi-entry output to disk, dev builds a single function in-memory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-vite-dev-server branch from 6c22cb8 to 0de8724 Compare March 30, 2026 18:53
Copy link
Copy Markdown
Member

@yoannmoinet yoannmoinet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really not confident about the tests on the dev-server.

middleware(req, res, jest.fn());
await res.done;

expect(res.statusCode).toBe(500);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it 500 or 400 that we're expecting for a bad request?

middleware(req, res, jest.fn());
await res.done;

expect(res.statusCode).toBe(500);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

500?

middleware(req, res, jest.fn());
await res.done;

expect(res.statusCode).toBe(500);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

500?

expect(res.statusCode).toBe(500);
const body = JSON.parse(res.getBody());
expect(body.success).toBe(false);
expect(body.error).toContain('HTTP 403');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

403 now?

These tests are not re-assuring to be honest.

Copy link
Copy Markdown
Collaborator Author

@sdkennedy2 sdkennedy2 Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to return a 403 here so the developer gets a clear signal that their credentials are wrong rather than a generic 500. However, doRequest throws a plain Error with the status code embedded in the message string ("HTTP 403 Forbidden") rather than a structured error with a .status property. To forward upstream status codes cleanly, we'd need to refactor doRequest to throw a typed error class — which would be a broader change across the codebase. Would you prefer we tackle that refactor as part of this PR or handle it as a follow-up?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a perspective of unblocking onboarding design partners I'd advocate we leave this as is.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to tackle this later.
I guess the error in question is this one?

bail(new Error(errorMessage));

Comment on lines +186 to +188
const maxRetries = 10;

for (let attempt = 0; attempt < maxRetries; attempt++) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this all covered by doRequest already?
I don't understand what this does that doRequest doesn't support yet.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These serve different purposes. doRequest handles HTTP-level retries — transient network failures, 5xx responses, etc. The poll loop here is application-level: the long-polling endpoint returns HTTP 200 with { done: false } when its server-side wait window (~30s) expires before the query finishes. We re-poll with a fresh request until done: true. doRequest can't express this because the HTTP request itself succeeds — it's the response payload that says "not ready yet, ask again."

Additionally, doRequest doesn't expose enough structure in its error handling to support this pattern — it throws a plain Error with the status code baked into the message string rather than a typed error object, so there's no way to programmatically distinguish between "server said not ready" vs "server returned an error" without string parsing. The poll loop gives us clean control over both cases. I'll add a comment to make the distinction clearer in the code.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, maybe a comment explaining this would be helpful.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call 👍. I added a comment.

if (!fullAuth) {
sendError(
res,
503,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

403 seems more appropriate here.

Copy link
Copy Markdown
Member

@yoannmoinet yoannmoinet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants