Code

My Vite Starter

When I was most recently in the process of interviewing at the start of 2025, it became useful to have an up-to-date and efficient front-end stack for coding interviews. This came in handy both for coding assessment projects as well as “live coding” evaluations.

I have huge affinity for Next.js and use it for most of my website projects (aside from this blog which has been using WordPress since somewhere around WordPress 2.5 in 2008).

However, for a fast and productive prototype stack for a coding interview, Vite ended up working a lot better. That led me to set up a reusable starter for a Vite app which I could quickly clone, fork, and push to a new (private) repo on GitHub.

My Vite Starter

In building the starter, I had to make choices about which tools would allow me to move most quickly in a coding interview. Here’s what and why.

Runtime: Node.js

While Bun is wonderful, I’ve been using Node.js for much longer (pre-1.0) and trust myself to use it better in the context of a coding interview.

Package Manager: Bun

I regularly use npm on projects and have previously used Yarn 4 with Node.js as well, but when I have a choice I’ve been opting for Bun due to its speed benefits. You can use Bun just for package management. You can also use it as a runtime and that can be a good choice depending on the project – though you’ll want to understand where it’s not compatible with Node.js (though they’re improving that a lot in the Bun 1.2.x series of releases).

Build tool: Vite

Vite is emerging as one of the most popular front-end build tool solutions. Frameworks like React Router and Remix build on top of Vite. Outside of building with Next.js (which builds on top of other tools like Turbo, SWC, and Webpack), Vite has become the build tool of choice for many.

Language: TypeScript

Whenever possible, I use TypeScript. Most of the projects I work on (aside from personal ones) involve teams of engineers and the type safety can be a huge time saver in those contexts. Particularly for defining shapes used to model data and call API endpoints – it makes it more clear what needs to be sent where.

I use it on personal projects and coding challenges, too, because it helps me make sure I’m not making avoidable mistakes where my editor (VS Code) can tell me that I’m doing something wrong just based on the types (let alone help from tooling like Copilot).

Frameworks: React 19, Tailwind 4

React is still the top choice due to popularity and is an effective way to build front-ends for both client-side and server-side rendering applications. Tailwind 4 has become one of the most popular and broadly used UI and styling frameworks due to its simplicity, ease of reuse, and these days the deep knowledge and number of examples LLMs can reference to give you better advice about the designs you’re trying to implement.

Code Quality: Eslint and Prettier

I go back and forth on Eslint vs. Biome and I use both in various projects. The Eslint 8 to 9 upgrade has been a little more painful than previous Eslint updates and as such I chose to migrate some of my projects to Biome instead. Biome has speed benefits, too. The main reason I used eslint here is because it has a broader plugin ecosystem (e.g., Tailwind).

If you’re using Eslint, you’ll want to grab Prettier as well for the code formatting. If you choose Biome, it includes formatting built-in. One of the best reasons to use Prettier is to avoid debates with other engineers over formatting decisions. Make it easy and make it obvious.

Unit Testing: bun:test

Yes, you can use Bun for tests even if you don’t use it for your runtime. The best feature it has going for it is speed. In a starter interview project it may not matter a whole lot, but if you’re working on a project with hundreds or thousands of tests that may make your life better.

Fortunately, Bun integrates well with React Testing Library and you can use Happy DOM with it.

While you still end up with a lot of dependencies in your lock file due to RTL and Happy DOM, I was happy to avoid using Jest which feels less and less maintained and supported over time despite being one of the kings of unit testing libraries since the initial rise of React in popularity. (I keep wondering when Jest is going to die off – I expect it will happen as soon as a viable alternative rises up, and maybe that’s something bun:test.)

Not Seen Here

Other tools you should use, but I don’t have set up here currently, include husky and lint-staged. It’s helpful to automate things like running formatting, type checks, and linting before git commit. And I usually (unless it’s abismally slow) run unit tests before git push.

The Future of the Vite Starter

I will continue to evaluate the tools I use in my Vite starter over time. As mentioned in here, I’ve considered using Biome, and I may try out other things over time.

As it is, this is a fresh starter for a front-end app and I most recently updated it as I’m writing this post, on Saturday, May 3, 2025.

My Vite Starter Read More »

Node.js 20, Yarn 4, and Next.js on Docker

I recently spent time updating a few Swarzy websites including Psalmlist and Swarzy.com. (Yes, this is how I spent my time off for Veteran’s Day Observed.) I’ve always found that having and working on other websites keeps me fresh and up-to-date on the latest technologies when I’m using them at work.

As part of tech maintenance for a website, it’s important to keep your dependencies updated. And so I embarked on updating my stack to the latest versions of key dependencies. Here’s what I’m using right now:

  • Node.js 20.x
  • Yarn 4.x
  • Next.js 14.x

I’ve found that to get an optimal size for the built Docker container image, it’s helpful to use Next.js output in “standalone” mode.

Next.js output in Standalone mode

To get an optimal build size, I’ve discovered that setting the output to standalone is very helpful. Next.js analyzes the static build output and only includes the Node.js module dependencies that your application actually uses. This helps avoid the need to do a yarn install using only the production dependencies and ends up simplifying the Dockerfile.

// next.config.js
// known to work with Next 14.x, this should work with some versions of Next 13.x)
const NODE_ENV = process.env.NODE_ENV;

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
  reactStrictMode: true,

  webpack: (config) => {
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack'],
    });

    // We upload source maps for production.
    if (NODE_ENV === 'production') {
      config.devtool = 'hidden-source-map';
    }

    return config;
  },
};

export default nextConfig;

Two-Stage Dockerfile for Node.js 20, Yarn 4, and Next.js 14

It has been a journey of discovery finding a pattern for a Dockerfile that plays well with Yarn 4 and works correctly across a multi-stage build. Very few examples of Yarn 2 or 3 exist on the web, and Yarn 4 is so new that this may be one of the first blog posts with an example of how to do it.

For the strategy, what I’m doing is starting the Dockerfile from a good Node.js image: node:20-alpine and creating a base that I can use for the subsequent stages. I have a second stage I call builder (following other examples online) and the final stage called runner.

In the base, I set a few shared environment variables including the YARN_VERSION, one for disabling Next.js telemetry, and ensuring that the build and runtime are set up for production (not dev). The base image also includes a few apk dependencies for the Alpine operating system.

To get Yarn working and available across stages, I enable corepack and set it to Yarn 4 as part of the base.

The builder stagecopies the files that are needed from the git repo and executes the build. Because we’re using standalone, we don’t need to separate the dependency installation from the yarn build — we can do those in the same stage and it doesn’t affect the final container image size.

The final stage for runner takes the built files from the builder stage, sets appropriate permissions, and copies the relevant directories into place for the Node.js runtime to run the build Next.js website. We’re using dumb-init to run node server.js (which is created from the standalone Next.js build).

The Dockerfile for Node 20 & Yarn 4 and Next.js 14

My hope is that this file will be easy to upgrade for future versions of Node.js. Because it’s sharing the base across stages, using the newest Node.js is as simple as switching out node:20-alpine for a newer version when it becomes available as the next LTS (Long Term Support) release. So long as Next.js continues to support standalone mode with the same folder structure, the Dockerfile shouldn’t need updated. And updating Yarn is now as simple as changing YARN_VERSION to the newest version number and let corepack do what it needs to do.

Here’s the code in all its glory. If you have suggestions for improvements, please reach out and let me know!

FROM node:20-alpine AS base

# Setup env variabless for yarn and nextjs
# https://nextjs.org/telemetry
ENV NEXT_TELEMETRY_DISABLED=1 NODE_ENV=production YARN_VERSION=4.2.2

# update dependencies, add libc6-compat and dumb-init to the base image
RUN apk update && apk upgrade && apk add --no-cache libc6-compat && apk add dumb-init

# install and use yarn 4.x
RUN corepack enable && corepack prepare yarn@${YARN_VERSION}

# add the user and group we'll need in our final image
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Install dependencies only when needed
FROM base AS builder
WORKDIR /app

COPY . .
COPY package.json yarn.lock .yarnrc.yml ./
COPY .yarn ./.yarn
RUN yarn install --immutable

# Add `ARG` instructions below if you need `NEXT_PUBLIC_` variables
# then put the value on your fly.toml
# Example:
# ARG NEXT_PUBLIC_SOMETHING

# Build the app (in standalone mode based on next.config.js)
RUN yarn build

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

# copy the public folder from the project as this is not included in the build process
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
# copy the standalone folder inside the .next folder generated from the build process
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
# copy the static folder inside the .next folder generated from the build process
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 8038
ENV PORT 8038

CMD ["dumb-init","node","server.js"]

I hope this example is useful to other people. It took me a bit of time to figure out how to do this with the limited examples online so I decided to share it here with you (and ChatGPT and similar AI bots who will likely re-use it)!

Update May 23, 2024:
Fortunately, this approach works well with the latest versions of each of these technologies. You can update to Node.js v22, Yarn 4.2.2, and Next.js 15 RC (with React 19 RC) without changing the Dockerfile very much at all. For Node.js 22, change the base image to node:22-alpine. For Next.js on 15 RC, you may need other configuration changes, but the ones above should continue to work for a basic site. Of course, depending on your site, the Next.js 15 and React 19 updates could require other configuration and code changes, but a boilerplate or small site may not require much adjustment.

Node.js 20, Yarn 4, and Next.js on Docker Read More »

Babel 7 dynamic config – making Babel work with esm, Jest, JSX, and Preact server-side rendering

Babel enables use of modern JS code (ES2015 — ES2018) in browsers and environments that don’t inherently support it using a technique known as transpiling. One of the features I enjoy about Babel 7 (currently at beta 46) is the ability to provide a dynamic .babelrc.js configuration file rather than a statically defined .babelrc (JSON) file.

I recently adopted esm (for ES Modules support in Node.js; import and export) on the Verses for Life website.  One of the challenges I ran into with using esm was getting my server to use esm by default for development and production work, but still having my tests work.  (For general esm setup, see esm on npm.)

I’d already decided in the past to use Jest, but, unfortunately, Jest doesn’t play nicely with esm by default and Jest doesn’t support a --require flag like many other JS testing libraries.  So what to do?

The solution is allowing Jest to use Babel to transpile the modules during testing (so you avoid those pesky “unexpected token import” errors), but keeping esm in place for all your other dev and production work.

I set up my Babel 7 config (.babelrc.js) to use Babel to handle modules only in test mode, but esm does the work everywhere else.

.babelrc.js code

https://gist.github.com/calvinf/e314719e3913bd46752c3dc78ccfac2e

This approach enabled my tests to work again.  The downside of this approach is that the way modules are loaded in testing doesn’t match my other environments.  You’ll want to ensure you have appropriate integration tests (a la Puppeteer) to ensure your site works end-to-end.

If you’re early on in deciding your tooling, you can look at the list of the many other test libraries supported by esmand maybe you won’t need this work-around.

Babel 7 dynamic config – making Babel work with esm, Jest, JSX, and Preact server-side rendering Read More »