Nuxt 3 First Impressions from a Next.js User
Tags: nuxt, nextjs, vue
Published: 2023-08-11 17:07:13

I'd been curious to check out [Vue](https://vuejs.org/) and [Nuxt](https://nuxt.com/) for a while. Last month, I finally had a chance to start reading up on it, and experiment with rebuilding a couple of sites that I had originally built with Next.js 12 and Preact.

From my initial experience, there are certainly a few things I really like, but there are also a few things I'm wary of.

## Built-in CSS module typings

In a [previous post](https://dev.to/kgf/supercharging-css-modules-with-typescript-1gnl), I explained how to generate specific typings for CSS module classes in Next.js. With Vue SFCs (both with or without Nuxt), you don't have to perform any special setup for this beyond installing the Volar extension for Visual Studio Code and [setting it up in your workspace](https://vuejs.org/guide/typescript/overview.html#volar-takeover-mode) - it immediately works out-of-the-box.

![A CSS module block in a Vue SFC, with a header class, including an indication of how many times it is referenced](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zexg9dy0zzk2livvdtsm.png)

![A template block in a Vue SFC, showing the header class in use, and showing the auto-generated typings based on the CSS module block](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7ib6gm6lp2rpiey6t6zh.png)

## Built-in API endpoint typings (and simpler API routes)

Something that quickly stood out to me about Nuxt, coming from Next.js, was that it uses [Nitro](https://nitro.unjs.io/) which provides a very "make simple things easy and hard things possible" approach to API routes. Not only is it extremely easy to define API endpoints that return JSON; when you do so, you also automatically get typings for their responses when using Nuxt's data fetching APIs.

Any `defineEventHandler` callback that returns a value results in an API route that returns the equivalent JSON:

```ts
export default defineEventHandler((event) => {
  return { hello: "world" };
});
```

When you use `useFetch` or `$fetch` within a Vue component for an API route defined in this way, you'll immediately get autocomplete and typings:

![Line of code reading `const hello = await useFetch("")`, with the text cursor inside the quotes, and an autocomplete menu open with an `/api/hello` entry](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/egay8kvagt9m1ppzx47u.png)

![Intellisense for the `hello` variable, picking up the `{ hello: string }` typing from the return value of the API route](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rfbi4utyhqrrykyd3vlh.png)

**Note:** Your dev server must be running in order to generate these typings.

This is a huge developer experience boost, saving you from redundantly specifying API response types on both server and client (or dealing with `any` implicitly on the client).

## useFetch as a replacement for getServerSideProps

It took me a moment to realize that _everything_ in Vue SFCs will run both server- and client-side by default, much like the default-exported component in a Next.js page module. There isn't really a direct equivalent to `getServerSideProps`, but we can make use of API routes and Nuxt's isomorphic `useFetch` API for that purpose.

When `useFetch` runs server-side, it directly calls the underlying API function with no extra network request. Additionally, it will automatically refresh client-side when any reactive references involved in the URL change (e.g. changing query parameters based on form inputs). You can explicitly instruct `useFetch` to watch other reactive references as well.

The absence of a more tangible boundary a la `getServerSideProps` means you don't need to explicitly pass along data from server-side methods to component props. Once you store the result of a `useFetch` call within `<script setup>`, it's available for use in the component template. This might also save you some effort exporting explicit typings, since you have no need to declare typings for component props for data received from the server - the types can flow directly through.

**Note:** In addition to tucking server-side logic into API routes, the `process` object available in components includes boolean `server` and `client` values, which can be used to conditionalize component logic to run during SSR or CSR but not both.

### Example: Hooking up a search API to a query parameter

Let's say you have a web application with a search form that updates a list of results within the page, and reflects the current query via a GET parameter. This use case involves some unique considerations, since the same component instance remains mounted when only URL parameters change but the route remains the same.

(**Bonus:** If you set your form's `action` to the URL path of your search route, and your input's `name` matches the query parameter used, the approach outlined in this example should work both with and without JavaScript enabled.)

First, you'd want to initialize two refs based on the query parameter: one for the currently active query, and another to synchronize with the form's input via [`v-model`](https://vuejs.org/guide/components/v-model.html):

```ts
const q = ref(useRoute().query.q);
const inputQuery = ref(q.value);
```

When the form is submitted, you'll want to update this value via a function hooked up to the form via `@submit.prevent`:

```ts
const onSubmit = () => {
  q.value = inputQuery.value;
};
```

Meanwhile, to populate your results, you'll want to call your API using the currently active query:

```ts
const { data, error } = await useFetch("/api/search", {
  query: { q },
  onResponse() {
    if (process.client)
      // Sync browser URL when client-side request completes.
      // (navigateTo can't be called directly within onResponse)
      nextTick(() => {
        const query = q.value ? { q: q.value } : {};
        navigateTo({
          path: "/",
          query,
        });
      });
  },
});
```

Notice that we pass the `q` ref - **not** its unwrapped value - directly to `query`. By doing this, Nuxt is aware that it should re-fetch any time `q` changes - i.e. when we submit the search form.

Finally, to keep state in sync upon browser history state changes:

```ts
onBeforeRouteUpdate(async (to) => {
  q.value = inputQuery.value = to.query.q || "";
});
```

## Module resolution surprises

For some reason, in each app I experimented with porting, part of Nuxt's stack seemed to disagree with lodash's module format.

In a project that uses lodash only server-side, everything worked in dev, but broke when I tried running the production server. I resolved this by adding lodash to the [`transpile` build option](https://nuxt.com/docs/api/configuration/nuxt-config#transpile) in `nuxt.config.ts`:

```ts
export default defineNuxtConfig({
  // ...
  build: {
    transpile: ["lodash"]
  },
  // ...
});
```

In another project that uses lodash both server- and client-side, I additionally ran into an issue where referencing default exports from lodash's individual modules (e.g. `import uniq from "lodash/uniq"`) didn't seem to work, even with `esModuleInterop: true` included in my TypeScript `compileOptions`. I was able to resolve this issue by switching to [lodash-es](https://www.npmjs.com/package/lodash-es).

I'm quite puzzled by these issues, since lodash is an exceedingly common dependency, and I did not encounter similar issues with any other dependencies. I really want to think I overlooked something...

## Cryptic errors

When I encountered the aforementioned lodash issues, as well as a few others that cropped up, I often found that Nuxt's errors were not terribly helpful. The stack trace would be buried within Nuxt's own lifecycle without reflecting where the error originated in my own code. Sometimes there would be no stack trace at all. Perhaps I was just unlucky with the types of errors I encountered?

## Documentation runs deep

Nuxt incorporates and builds upon several libraries, which means there's a lot of ground to cover when looking for documentation:

- [Nuxt](https://nuxt.com/docs/getting-started/introduction)
- [Vue](https://vuejs.org/guide/introduction.html) (for components and composables)
- [Vue router](https://router.vuejs.org/introduction.html) (which Nuxt's routing is based upon)
- [Nitro](https://nitro.unjs.io/guide/getting-started) (for the `server` directory, e.g. API routes)
- [h3](https://www.jsdocs.io/package/h3) (for helpers available in API routes)
- [ofetch](https://github.com/unjs/ofetch#readme) (for data fetching APIs)

You won't necessarily need to know everything about every single piece of the puzzle, but keep in mind that sometimes particular details you're looking for may originate in one of the more-specific libraries, rather than in Nuxt's own docs. (Fortunately, they cross-link in some of the most relevant places.)

## Conclusion

I was pretty excited by the developer experience benefits that I discovered and outlined in this post. That being said, I'm wary of committing to using Nuxt on larger-scale apps given how much head-scratching ensued from some of the errors and snags I encountered. There's also the matter of _everything_ under `components` being available as auto-imports by default; I'd probably want to fine-tune that behavior for a larger project, lest it pollute autocomplete with a bunch of stuff that's irrelevant most of the time.

I've also more recently started experimenting with [Qwik](https://qwik.builder.io/), now that I realized it also has a meta-framework in the form of Qwik City. I've already experimented with porting one of my projects to it and it seems promising; I might write up thoughts on that separately after trying it on both projects I tested with Nuxt.

Given some questionable articles I've seen lately, I feel compelled to point out that I'm not simply chasing these frameworks in the name of "ooh shiny"; I firmly believe that an existing framework being "old" is not nearly sufficient reason to bail on it. However, I've seen enough folks hinting that React has been late to the party with some useful features, and I've also been wary of the complex direction Next 13 is headed in, so I've wanted to see what benefits are unique to other frameworks. That being said, Next 12 had some great features and the best developer experience I had encountered at that point, and I imagine that React will continue to have critical mass for a while.