N
Nuxt2mo ago
tim

How to test Nuxt 3 Nitro Server endpoints and middlewares (avoiding defineEventHandler not defined)

Hi, how would you test a Nuxt server api endpoint or middleware? I do struggle with ReferenceError: defineEventHandler is not defined errors. defineEventHandler is a Nitro-specific runtime utility that is not available in my test environment. The way my test is currently set up, I am trying to execute the middleware file directly, but defineEventHandler is not properly mocked. How could I do that? I haven't found any working online resources for examples of testing Nuxt server routes / Nitro routes with Vitest in a Nuxt project :/ Would be very happy if anyone could look into my setup or provide me with resources so that I can get my setup to work
FAIL ../tests/unit/server/middleware/visibilityGuard.global.spec.ts [ ../tests/unit/server/middleware/visibilityGuard.global.spec.ts ]
ReferenceError: defineEventHandler is not defined
❯ ../server/middleware/visibilityGuard.global.ts:5:1
3| import { sendRedirect } from 'h3';
4|
5| export default defineEventHandler((event: H3Event<EventHandlerRequest>) => {
| ^
6| const runtimeConfig: NitroRuntimeConfig = useRuntimeConfig(event);
7| const appState: string = runtimeConfig.APP_STATE || 'maintenance';
❯ ../tests/unit/server/middleware/visibilityGuard.global.spec.ts:6:1
FAIL ../tests/unit/server/middleware/visibilityGuard.global.spec.ts [ ../tests/unit/server/middleware/visibilityGuard.global.spec.ts ]
ReferenceError: defineEventHandler is not defined
❯ ../server/middleware/visibilityGuard.global.ts:5:1
3| import { sendRedirect } from 'h3';
4|
5| export default defineEventHandler((event: H3Event<EventHandlerRequest>) => {
| ^
6| const runtimeConfig: NitroRuntimeConfig = useRuntimeConfig(event);
7| const appState: string = runtimeConfig.APP_STATE || 'maintenance';
❯ ../tests/unit/server/middleware/visibilityGuard.global.spec.ts:6:1
6 Replies
kapa.ai
kapa.ai2mo ago
Hi @tim. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
tim
timOP2mo ago
thats my middleware (server/middleware/visibilityGuard.global.ts):
import type { EventHandlerRequest, H3Event } from 'h3';
import type { NitroRuntimeConfig } from 'nitropack';
import { sendRedirect } from 'h3';

export default defineEventHandler((event: H3Event<EventHandlerRequest>) => {
const runtimeConfig: NitroRuntimeConfig = useRuntimeConfig(event);
const appState: string = runtimeConfig.APP_STATE || 'maintenance';

switch (appState) {
case 'development':
break;
case 'production':
if (event.path === '/maintenance' || event.path === '/coming-soon') {
return sendRedirect(event, '/');
}
break;
case 'coming':
if (event.path !== '/coming-soon') {
return sendRedirect(event, '/coming-soon');
}
break;
case 'maintenance':
if (event.path !== '/maintenance') {
return sendRedirect(event, '/maintenance');
}
break;
default:
break;
}
});
import type { EventHandlerRequest, H3Event } from 'h3';
import type { NitroRuntimeConfig } from 'nitropack';
import { sendRedirect } from 'h3';

export default defineEventHandler((event: H3Event<EventHandlerRequest>) => {
const runtimeConfig: NitroRuntimeConfig = useRuntimeConfig(event);
const appState: string = runtimeConfig.APP_STATE || 'maintenance';

switch (appState) {
case 'development':
break;
case 'production':
if (event.path === '/maintenance' || event.path === '/coming-soon') {
return sendRedirect(event, '/');
}
break;
case 'coming':
if (event.path !== '/coming-soon') {
return sendRedirect(event, '/coming-soon');
}
break;
case 'maintenance':
if (event.path !== '/maintenance') {
return sendRedirect(event, '/maintenance');
}
break;
default:
break;
}
});
kapa.ai
kapa.ai2mo ago
---------------------- To test Nuxt 3 Nitro server endpoints and middlewares, you need to set up a proper testing environment that includes the Nuxt runtime. The error you're encountering (defineEventHandler is not defined) occurs because you're trying to import the server middleware directly without the Nuxt environment that provides these auto-imports. Here's how you can properly test Nitro server endpoints and middlewares: Setting up the test environment 1. First, install the required dependencies:
npm i --save-dev @nuxt/test-utils vitest @vue/test-utils happy-dom playwright-core
npm i --save-dev @nuxt/test-utils vitest @vue/test-utils happy-dom playwright-core
2. Add the Nuxt test module to your nuxt.config.ts (optional but recommended):
export default defineNuxtConfig({
modules: [
'@nuxt/test-utils/module'
]
})
export default defineNuxtConfig({
modules: [
'@nuxt/test-utils/module'
]
})
3. Create a vitest.config.ts file:
import { defineVitestConfig } from '@nuxt/test-utils/config'

export default defineVitestConfig({
test: {
environment: 'nuxt'
}
})
import { defineVitestConfig } from '@nuxt/test-utils/config'

export default defineVitestConfig({
test: {
environment: 'nuxt'
}
})
Testing server endpoints For testing server endpoints, you have a few options: Option 1: Use the Nuxt environment Create a test file with .nuxt.test.ts extension or add // @vitest-environment nuxt comment at the top of your test file:
// @vitest-environment nuxt
import { describe, test, expect } from 'vitest'

describe('Server middleware', () => {
test('visibilityGuard middleware', async () => {
// Your test code here
})
})
// @vitest-environment nuxt
import { describe, test, expect } from 'vitest'

describe('Server middleware', () => {
test('visibilityGuard middleware', async () => {
// Your test code here
})
})
Option 2: Use registerEndpoint for mocking endpoints You can use registerEndpoint from @nuxt/test-utils/runtime to mock endpoints:
import { registerEndpoint } from '@nuxt/test-utils/runtime'
import { describe, test, expect } from 'vitest'
describe('API endpoint', () => {
test('should return expected data', async () => {
registerEndpoint('/api/test', () => ({
test: 'test-field'
}))

// Now test your component that calls this endpoint
})
})
import { registerEndpoint } from '@nuxt/test-utils/runtime'
import { describe, test, expect } from 'vitest'
describe('API endpoint', () => {
test('should return expected data', async () => {
registerEndpoint('/api/test', () => ({
test: 'test-field'
}))

// Now test your component that calls this endpoint
})
})
Option 3: Mount your Nuxt app and test the actual endpoint
// @vitest-environment nuxt
import { describe, test, expect } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'

describe('API endpoint', () => {
test('should return expected data', async () => {
// Test the actual endpoint
const response = await $fetch('/api/your-endpoint')
expect(response).toMatchObject({ /* expected response */ })
})
})
// @vitest-environment nuxt
import { describe, test, expect } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'

describe('API endpoint', () => {
test('should return expected data', async () => {
// Test the actual endpoint
const response = await $fetch('/api/your-endpoint')
expect(response).toMatchObject({ /* expected response */ })
})
})
Specific to your error For your specific case with the visibilityGuard.global.ts middleware, you need to ensure you're testing within a Nuxt environment. Here's how you might structure your test:
// @vitest-environment nuxt
import { describe, test, expect, vi } from 'vitest'
import { sendRedirect } from 'h3'

// Mock the sendRedirect function
vi.mock('h3', async () => {
const actual = await vi.importActual('h3')
return {
...actual,
sendRedirect: vi.fn()
}
})

describe('visibilityGuard middleware', () => {
test('should redirect when app is in maintenance mode', async () => {
// Set up your test environment
// Test the middleware behavior

// You can verify if sendRedirect was called with the expected arguments
expect(sendRedirect).toHaveBeenCalledWith(/* expected args */)
})
})
// @vitest-environment nuxt
import { describe, test, expect, vi } from 'vitest'
import { sendRedirect } from 'h3'

// Mock the sendRedirect function
vi.mock('h3', async () => {
const actual = await vi.importActual('h3')
return {
...actual,
sendRedirect: vi.fn()
}
})

describe('visibilityGuard middleware', () => {
test('should redirect when app is in maintenance mode', async () => {
// Set up your test environment
// Test the middleware behavior

// You can verify if sendRedirect was called with the expected arguments
expect(sendRedirect).toHaveBeenCalledWith(/* expected args */)
})
})
The key is to use the Nuxt environment for your tests, which provides all the necessary auto-imports and context that your server middleware depends on. For more information, check out: - Nuxt Testing Documentation - Unit Testing in Nuxt ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
tim
timOP2mo ago
and thats my test file (tests/unit/server/middleware/visibilityGuard.global.spec.ts):
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { H3Event, EventHandlerRequest } from 'h3';
import { sendRedirect } from 'h3';
import visibilityGuard from '../../../../server/middleware/visibilityGuard.global';

vi.stubGlobal('defineEventHandler', (handler: any) => handler);
const runtimeConfigMock = vi.fn(() => ({ APP_STATE: 'maintenance' }));
vi.stubGlobal('useRuntimeConfig', runtimeConfigMock);

vi.mock('h3', async () => {
return {
sendRedirect: vi.fn(),
};
});

describe('visibilityGuard global middleware', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('should be a function', () => {
expect(typeof visibilityGuard).toBe('function');
});

const createEvent = (path: string): H3Event<EventHandlerRequest> => ({
path,
context: {}, // Mock necessary context
} as unknown as H3Event<EventHandlerRequest>);

it('redirects to /maintenance when app state is maintenance and path is not /maintenance', () => {
runtimeConfigMock.mockReturnValueOnce({ APP_STATE: 'maintenance' });
const event = createEvent('/dashboard');

visibilityGuard(event);

expect(sendRedirect).toHaveBeenCalledWith(event, '/maintenance');
});

// more test cases

it('does nothing (allows navigation) when app state is development', () => {
runtimeConfigMock.mockReturnValueOnce({ APP_STATE: 'development' });
const event = createEvent('/dashboard');

visibilityGuard(event);

expect(sendRedirect).not.toHaveBeenCalled();
});
});
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { H3Event, EventHandlerRequest } from 'h3';
import { sendRedirect } from 'h3';
import visibilityGuard from '../../../../server/middleware/visibilityGuard.global';

vi.stubGlobal('defineEventHandler', (handler: any) => handler);
const runtimeConfigMock = vi.fn(() => ({ APP_STATE: 'maintenance' }));
vi.stubGlobal('useRuntimeConfig', runtimeConfigMock);

vi.mock('h3', async () => {
return {
sendRedirect: vi.fn(),
};
});

describe('visibilityGuard global middleware', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('should be a function', () => {
expect(typeof visibilityGuard).toBe('function');
});

const createEvent = (path: string): H3Event<EventHandlerRequest> => ({
path,
context: {}, // Mock necessary context
} as unknown as H3Event<EventHandlerRequest>);

it('redirects to /maintenance when app state is maintenance and path is not /maintenance', () => {
runtimeConfigMock.mockReturnValueOnce({ APP_STATE: 'maintenance' });
const event = createEvent('/dashboard');

visibilityGuard(event);

expect(sendRedirect).toHaveBeenCalledWith(event, '/maintenance');
});

// more test cases

it('does nothing (allows navigation) when app state is development', () => {
runtimeConfigMock.mockReturnValueOnce({ APP_STATE: 'development' });
const event = createEvent('/dashboard');

visibilityGuard(event);

expect(sendRedirect).not.toHaveBeenCalled();
});
});
kapa.ai
kapa.ai2mo ago
<@1023182692575084605> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
tim
timOP2mo ago
package.json:
{
"name": "website",
"version": "v2025.3.1-dev",
"type": "module",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"preinstall": "npx only-allow pnpm",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test:unit": "vitest",
"test:watch": "vitest --watch",
"test:coverage": "vitest run --coverage",
"lhci:mobile": "lhci autorun",
"lhci:desktop": "lhci autorun --collect.settings.preset=desktop"
},
"engines": {
"node": ">=18.18.0",
"npm": ">=9.7.0"
},
"dependencies": {
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.2.0",
"@nuxt/devtools": "latest",
"@nuxt/types": "^2.18.1",
"@nuxtjs/i18n": "^9.2.1",
"@nuxtjs/tailwindcss": "^6.12.2",
"@nuxtjs/turnstile": "^0.9.11",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@types/node": "^22.13.9",
"@vueuse/core": "^12.7.0",
"motion-v": "0.10.1",
"nuxt-cloudflare-analytics": "^1.0.8",
"nuxt-headlessui": "^1.2.0",
"nuxt-security": "2.1.5",
"resend": "4.1.2"
},
"devDependencies": {
"@inspira-ui/plugins": "^0.0.1",
"@lhci/cli": "^0.14.0",
"@nuxt/eslint": "^1.0.1",
"@nuxt/scripts": "^0.10.4",
"@nuxt/test-utils": "^3.17.1",
"@nuxtjs/apollo": "5.0.0-alpha.14",
"@nuxtjs/supabase": "^1.4.4",
"@vitest/coverage-istanbul": "^3.0.3",
"@vue/test-utils": "^2.4.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"eslint": "^9.20.1",
"happy-dom": "^17.1.9",
"nuxt": "^3.14.1592",
"playwright-core": "^1.49.1",
"tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7",
"vitest": "^3.0.3"
}
}
{
"name": "website",
"version": "v2025.3.1-dev",
"type": "module",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"preinstall": "npx only-allow pnpm",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test:unit": "vitest",
"test:watch": "vitest --watch",
"test:coverage": "vitest run --coverage",
"lhci:mobile": "lhci autorun",
"lhci:desktop": "lhci autorun --collect.settings.preset=desktop"
},
"engines": {
"node": ">=18.18.0",
"npm": ">=9.7.0"
},
"dependencies": {
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.2.0",
"@nuxt/devtools": "latest",
"@nuxt/types": "^2.18.1",
"@nuxtjs/i18n": "^9.2.1",
"@nuxtjs/tailwindcss": "^6.12.2",
"@nuxtjs/turnstile": "^0.9.11",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@types/node": "^22.13.9",
"@vueuse/core": "^12.7.0",
"motion-v": "0.10.1",
"nuxt-cloudflare-analytics": "^1.0.8",
"nuxt-headlessui": "^1.2.0",
"nuxt-security": "2.1.5",
"resend": "4.1.2"
},
"devDependencies": {
"@inspira-ui/plugins": "^0.0.1",
"@lhci/cli": "^0.14.0",
"@nuxt/eslint": "^1.0.1",
"@nuxt/scripts": "^0.10.4",
"@nuxt/test-utils": "^3.17.1",
"@nuxtjs/apollo": "5.0.0-alpha.14",
"@nuxtjs/supabase": "^1.4.4",
"@vitest/coverage-istanbul": "^3.0.3",
"@vue/test-utils": "^2.4.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"eslint": "^9.20.1",
"happy-dom": "^17.1.9",
"nuxt": "^3.14.1592",
"playwright-core": "^1.49.1",
"tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7",
"vitest": "^3.0.3"
}
}
vitest.config.ts:
import { defineVitestConfig } from '@nuxt/test-utils/config';
import { loadEnv } from 'vite';

export default defineVitestConfig({
test: {
env: loadEnv('test', process.cwd()),
environment: 'nuxt',
environmentOptions: {
nuxt: {
mock: {
intersectionObserver: true,
indexedDb: true,
},
},
},
coverage: {
provider: 'istanbul',
reporter: ['text', 'json', 'html'],
reportsDirectory: './coverage',
all: true,
include: ['**/*.{ts,vue}'],
exclude: ['node_modules/', 'tests/'],
},
},
});
import { defineVitestConfig } from '@nuxt/test-utils/config';
import { loadEnv } from 'vite';

export default defineVitestConfig({
test: {
env: loadEnv('test', process.cwd()),
environment: 'nuxt',
environmentOptions: {
nuxt: {
mock: {
intersectionObserver: true,
indexedDb: true,
},
},
},
coverage: {
provider: 'istanbul',
reporter: ['text', 'json', 'html'],
reportsDirectory: './coverage',
all: true,
include: ['**/*.{ts,vue}'],
exclude: ['node_modules/', 'tests/'],
},
},
});

Did you find this page helpful?