Skip to content

API Integration ​

CleanSlice generates a fully typed API client from your backend's OpenAPI/Swagger specification using @hey-api/openapi-ts. This means your frontend SDK -- service classes, TypeScript types, and request/response schemas -- stays in sync with your NestJS API automatically.

How It Works ​

The flow is straightforward:

  1. Your NestJS API generates a swagger-spec.json file at startup
  2. You run bun run build:api in the Nuxt app
  3. @hey-api/openapi-ts reads the spec and generates TypeScript code
  4. Feature slices import the generated services and types via the #api alias
NestJS API                    Nuxt App
─────────                     ────────
swagger-spec.json  ──build:api──>  slices/setup/api/data/repositories/api/
                                    ├── client.gen.ts    (Axios client)
                                    ├── sdk.gen.ts       (Service classes)
                                    ├── types.gen.ts     (TypeScript interfaces)
                                    └── schemas.gen.ts   (JSON schemas)

Installation ​

bash
# Code generator (dev dependency)
bun add -d @hey-api/openapi-ts

# Runtime client
bun add @hey-api/client-axios axios

Add the build script to your package.json:

json
{
  "scripts": {
    "build:api": "openapi-ts",
    "dev": "bun run build:api && nuxt dev",
    "build": "bun run build:api && nuxt build"
  }
}

Configuration ​

openapi-ts.config.ts ​

This root-level config tells the generator where to find the spec and where to write the output:

typescript
import { defineConfig } from '@hey-api/openapi-ts';

export default defineConfig({
  name: 'ApiClient',

  // Path to your API's OpenAPI spec
  // For local dev, this points to the neighboring api folder
  input: '../api/swagger-spec.json',

  output: {
    format: 'prettier',
    lint: 'eslint',
    path: './slices/setup/api/data/repositories/api',
  },

  plugins: [
    {
      // Use Axios as the HTTP client
      name: '@hey-api/client-axios',
      runtimeConfigPath: './slices/setup/api/api.config.ts',
    },
    {
      // Generate TypeScript enums
      enums: 'typescript',
      name: '@hey-api/typescript',
    },
    {
      // Generate JSON schemas
      name: '@hey-api/schemas',
      type: 'json',
    },
    {
      // Generate class-based service SDK
      name: '@hey-api/sdk',
      asClass: true,
    },
  ],
});

api.config.ts ​

The client configuration sets the base URL for all API requests:

typescript
import type { CreateClientConfig } from './data/repositories/api/client.gen';

export const createClientConfig: CreateClientConfig = (config) => ({
  ...config,
  baseURL: process.env.API_URL ?? 'http://localhost:3333',
});

Slice nuxt.config.ts ​

The API slice registers the #api alias so other slices can import from it cleanly:

typescript
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const currentDir = dirname(fileURLToPath(import.meta.url));

export default defineNuxtConfig({
  alias: {
    '#api': currentDir,
  },
});

Environment Variables ​

Set API_URL in your .env file:

bash
API_URL=http://localhost:3333

For production builds, pass it as a build argument:

bash
API_URL=https://api.yourapp.com bun run build

Generated SDK Structure ​

Each API controller tag generates a service class:

API TagGenerated ClassExample Methods
AuthAuthServicelogin(), register(), me()
UsersUsersServicegetUsers(), getUser(), createUser()
FilesFilesServiceuploadFile(), getFiles(), deleteFile()

The generated files:

slices/setup/api/data/repositories/api/
├── client.gen.ts       # Axios client instance
├── sdk.gen.ts          # Service classes (AuthService, UsersService, etc.)
├── types.gen.ts        # All DTOs and response types
├── schemas.gen.ts      # JSON schemas for validation
└── index.ts            # Barrel export

WARNING

The files in data/repositories/api/ are auto-generated. Any manual edits are overwritten the next time you run bun run build:api.

Barrel Exports ​

The API slice re-exports generated code through barrel files so you can import everything from #api/data:

typescript
export * from './repositories';
typescript
export * from './api';
typescript
export * from './data';

Using the API SDK ​

Importing Services and Types ​

Always use the #api alias. The generated types and services need explicit imports (they are not auto-imported):

typescript
// Correct -- use the alias
import { AuthService, UsersService, type UserDto } from '#api/data';

// Wrong -- relative paths break when slices move
import { AuthService } from '../../../setup/api/data/repositories/api';

Calling API Methods ​

typescript
import { AuthService, UsersService, type UserDto } from '#api/data';

// Login
const loginResponse = await AuthService.login({
  body: {
    email: '[email protected]',
    password: 'password123',
  },
});

// Get current user
const meResponse = await AuthService.me();
const user: UserDto = meResponse.data?.data;

// List users with pagination
const usersResponse = await UsersService.getUsers({
  query: { page: 1, limit: 10 },
});

Using in Stores ​

typescript
import { defineStore } from 'pinia';
import { AuthService, type AuthDto, type LoginUserDto } from '#api/data';
import { handleApiAuthentication } from '#api/utils/handleApiAuthentication';

export const useAuthStore = defineStore('auth', {
  state: () => ({
    auth: null as AuthDto | null,
    loading: false,
  }),

  actions: {
    async login(credentials: LoginUserDto): Promise<boolean> {
      try {
        this.loading = true;
        const response = await AuthService.login({
          body: credentials,
        });

        const authData = response.data?.data;
        if (authData) {
          this.auth = authData;
          handleApiAuthentication(authData.accessToken);
          return true;
        }
        return false;
      } catch (e) {
        console.error('Login failed', e);
        return false;
      } finally {
        this.loading = false;
      }
    },

    logout() {
      this.auth = null;
      handleApiAuthentication(); // Clears the auth header
    },
  },
});

Using in Components ​

vue
<script setup lang="ts">
import { UsersService, type UserDto } from '#api/data';

const { data, pending } = useAsyncData('users', () =>
  UsersService.getUsers()
);

const users = computed<UserDto[]>(() => data.value?.data?.data || []);
</script>

<template>
  <div v-if="pending">Loading...</div>
  <ul v-else>
    <li v-for="user in users" :key="user.id">
      {{ user.name }} - {{ user.email }}
    </li>
  </ul>
</template>

Authentication ​

Setting the Auth Header ​

The handleApiAuthentication utility adds or removes the Authorization header on the Axios client:

typescript
import { client } from '../data/repositories/api/client.gen';

export const handleApiAuthentication = (token?: string) => {
  if (token) {
    client.instance.defaults.headers.common['Authorization'] =
      `Bearer ${token}`;
  } else {
    delete client.instance.defaults.headers.common['Authorization'];
  }
};

API Interceptor Plugin ​

The API plugin sets up Axios interceptors that catch errors globally and route them through the error handling system:

typescript
import { client } from '../data/repositories/api/client.gen';
import { defineNuxtPlugin } from '#app';
import type {
  AxiosError,
  AxiosResponse,
  InternalAxiosRequestConfig,
} from 'axios';

export default defineNuxtPlugin((nuxtApp) => {
  client.instance.interceptors.request.use(
    (config: InternalAxiosRequestConfig) => config,
    (error: AxiosError) => handleError(error)
  );

  client.instance.interceptors.response.use(
    (response: AxiosResponse) => response,
    (error: AxiosError) => handleError(error)
  );

  return { provide: { client } };
});

The handleError function is auto-imported from the error slice. It handles 401 token refresh, translates error codes via i18n, and shows toast notifications.

Available Type Exports ​

The generated SDK exports all DTOs, response types, error types, and enums:

typescript
import {
  // DTOs
  type UserDto,
  type CreateUserDto,
  type UpdateUserDto,
  type LoginUserDto,
  type RegisterUserDto,
  type AuthDto,

  // Response types
  type GetUsersResponse,
  type GetUserResponse,
  type LoginResponse,

  // Enums
  UserRole,
  UserStatus,
} from '#api/data';

Regenerating the SDK ​

Run bun run build:api whenever:

  • Backend API endpoints change
  • DTOs are added or modified
  • New controllers are added
  • Swagger decorators are updated

Development Workflow ​

bash
# Terminal 1: Run the API (generates swagger-spec.json on start)
cd api && bun run start:dev

# Terminal 2: Run the app (regenerates SDK, then starts Nuxt)
cd app && bun run dev

TIP

For CI/CD pipelines, you can fetch the spec from a URL instead of a local file:

typescript
input: process.env.SWAGGER_URL || '../api/swagger-spec.json',

What's Next? ​

Built with CleanSlice