Learn Nuxt Icon

Nuxt 3 auto-imports magic

Since v3, Nuxt introduced the concept of auto imports, essentially this is a bunch of functions from Nuxt and other libraries that you can use in your code without importing it, for example:

~/middleware/auth.ts


// ? Where is this function coming from ?
export default defineNuxtRouteMiddleware(() => {})

In this article we are going to look at the implementation of auto-imports and as a bonus, we'll also look at how you can explicitly import these functions like:

~/middleware/auth.ts


import { defineNuxtRouteMiddleware } from "#imports"
export default defineNuxtRouteMiddleware(() => {})

In the next article (with a deeper understanding of auto-imports) we'll start to look at Nuxt specific auto-imports.

How does auto-import magic work anyway? Let's build it from scratch

Auto imports is technically not just a Nuxt feature, but a feature of Typescript + your bundler. To get a better sense of how it works, let's step away from Nuxt for a moment and build a small example with auto-imports.

Project initialization

Create the new folder and make a package.json in it with pnpm:

~


mkdir auto-imports && cd auto-imports && pnpm init

Next, install TypeScript:

~/auto-imports


pnpm add -D typescript

Getting started with our auto-imports logic

The next part is what Nuxt does in a build step. We're going to manually create files in a .app folder, this is equivalent to what Nuxt builds into its .nuxt folder. We are going to make a logger function - that auto-imports into all the files in our projects.

Let's start with a index.ts in our project root so we can define the function we want to work:

~/auto-imports/index.ts


logger("hello world!")

That's it! No importing of logger. At this point - TypeScript should be screaming at you that it cannot find logger. Let's fix that.

At this point, your file structure should look like this:

~/auto-imports


| - node_modules
| - index.ts
| - package.json
| - pnpm-lock.yaml

Creating your own auto-import

Let's define our project's tsconfig. I say our because it will extend the built "module" that we'll simulate just like nuxt does:

~/auto-imports/tsconfig.json


{
  "extends": "./.app/tsconfig.json"
}

Now TypeScript should also be complaining that it cannot find this config, and this is exactly why nuxt has the npx nuxi prepare command - to generate all these necessary types from your project. We'll do it manually for our project though and instead of a .nuxt directory we'll use a .app directory. Just remember that we're simulating this folder as a built folder.

~/auto-imports


mkdir .app

Let's start with creating the actual logger function that the import will refer to, create a file called logger.ts:

~/auto-imports/.app/logger.ts


export const logger = (input: string) => console.log(input);

Now we want to be able to reference this logger in our app in two ways:

  1. Globally (auto-imported)
  2. imported from #imports

Let's start by creating a imports.d.ts file that will hold all our declarations that should be available in #imports:

~/auto-imports/.app/imports.d.ts


export { logger } from "./logger"

Our file is pretty small, we only have 1 global function, but we could technically export anything here that we want to provide via #imports in our app.

Next, let's define a globals.d.ts that we'll use to tell our project 'after all is said and done - the stuff in here will be available globally'. It's basically just a trick because we know at the end when everything gets compiled this function will be compiled into the scope where it's used, so TypeScript shouldn't error if we don't specify the import in our pre-built code:

~/auto-imports/.app/globals.d.ts


export {}

declare global {
  const logger: typeof import("./logger")["logger"]
}

This is importing the logger definition and putting its type on the global scope. We need the export {} so TS recognizes this module.

The glue to it all

To tie it all together, we need a 3rd file before creating the tsconfig that our app imports - this is the reference file and we'll name it app.d.ts:

~/auto-imports/.app/app.d.ts


/// <reference path="imports.d.ts" />
/// <reference path="globals.d.ts" />

export {}

You can read more on this syntax here. In short, they tell ts what to include and also specify the exact order if using an out file. For our purposes, we're using it as a single point of entry for our grand finale - the tsconfig.json.

The tsconfig.json to rule them all

~/auto-imports/.app/tsconfig.json


{
  "compilerOptions": {
    "baseUrl": "..",
    "paths": { "#imports": ["./.app/imports"] }
  },
  "include": ["../**/*", "./app.d.ts"]
}

This is what brings the magic to your app. Remember that your tsconfig is referencing this one. This tsconfig is saying that #imports should reference the imports.d.ts file, and it's including app.d.ts which has the reference to all our global and other types. It is also including all the types of our app. Now in our app we should be able to use:

~/auto-imports/index.ts


logger("hello world!")

And also use:

~/auto-imports/index.ts


import {logger} from "#imports";

logger("hello world!")

Without any TS issues ⚡️.

If you are still getting TS errors, restart your IDE or TypeScript server first before trying to debug.

Final project structure

For reference this is what your folder / file structured should look like at the end of this tutorial:


| - .app
  | - app.d.ts
  | - globals.d.ts
  | - imports.d.ts
  | - logger.ts
  | - tsconfig.json
| - node_modules
| - index.ts
| - package.json
| - pnpm-lock.yaml
| - tsconfig.json

I hope this part of the guide was helpful, demistifying things like this is fun and think we should all take some time to explore and ask questions about the code we're writing. In the next article we'll finally look at some of Nuxt's own auto-imports and where we use them / what they do.