React & Next.js Integration Guide
This guide covers integrating SpecJet with React and Next.js applications, including setup, configuration, and usage patterns.
Quick Setup
1. Install SpecJet
# In your React/Next.js project
npm install --save-dev specjet
# Or globally
npm install -g specjet
2. Initialize SpecJet
# Initialize in your existing project
specjet init .
# This creates:
# ├── api-contract.yaml      # Your API contract
# ├── specjet.config.js      # Configuration
# └── src/
#     ├── types/             # Generated TypeScript types
#     ├── api/               # Generated API client
#     └── mocks/             # Generated mock server
3. Configure for React
Update specjet.config.js for React conventions:
export default {
  contract: './api-contract.yaml',
  
  output: {
    types: './src/types/api',    // Group API types together
    client: './src/lib/api'      // Common lib pattern
  },
  
  typescript: {
    strictMode: true,
    exportType: 'named',         // Named exports for tree-shaking
    clientName: 'ApiClient'
  },
  
  mock: {
    port: 3001,                  // Avoid conflict with React dev server
    cors: {
      origin: ['http://localhost:3000'], // React dev server
      credentials: true
    },
    scenario: 'realistic'
  }
};
4. Add Package.json Scripts
{
  "scripts": {
    "dev": "next dev",
    "api:generate": "specjet generate",
    "api:watch": "specjet generate --watch",
    "api:mock": "specjet mock",
    "dev:with-mock": "concurrently \"npm run dev\" \"npm run api:mock\""
  },
  "devDependencies": {
    "concurrently": "^7.6.0",
    "specjet": "^0.1.0"
  }
}
Environment Configuration
Development vs Production
Create environment-aware API configuration:
// src/lib/api/config.ts
import { ApiClient } from './client';
const getApiBaseUrl = (): string => {
  // Next.js environment variables
  if (process.env.NODE_ENV === 'development') {
    return process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
  }
  
  if (process.env.NODE_ENV === 'production') {
    return process.env.NEXT_PUBLIC_API_URL || 'https://api.myapp.com';
  }
  
  // Testing
  return 'http://localhost:3001';
};
export const api = new ApiClient(getApiBaseUrl());
// Optional: Add authentication helpers
export const setAuthToken = (token: string) => {
  api.setBearerToken(token);
};
export const clearAuth = () => {
  api.setAuth(null);
};
Environment Variables
# .env.local (development)
NEXT_PUBLIC_API_URL=http://localhost:3001
# .env.production
NEXT_PUBLIC_API_URL=https://api.myapp.com
React Hooks Integration
Basic API Hook
Create reusable hooks for API calls:
// src/hooks/useApi.ts
import { useState, useEffect } from 'react';
import { api } from '../lib/api/config';
import type { User, CreateUserRequest } from '../types/api';
export function useUsers() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  useEffect(() => {
    async function fetchUsers() {
      try {
        setLoading(true);
        const userData = await api.getUsers();
        setUsers(userData);
        setError(null);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Failed to fetch users');
        setUsers([]);
      } finally {
        setLoading(false);
      }
    }
    fetchUsers();
  }, []);
  const createUser = async (userData: CreateUserRequest): Promise<User | null> => {
    try {
      const newUser = await api.createUser(userData);
      setUsers(prev => [...prev, newUser]);
      return newUser;
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to create user');
      return null;
    }
  };
  const updateUser = async (id: number, userData: Partial<User>): Promise<User | null> => {
    try {
      const updatedUser = await api.updateUser(id, userData);
      setUsers(prev => prev.map(user => user.id === id ? updatedUser : user));
      return updatedUser;
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to update user');
      return null;
    }
  };
  const deleteUser = async (id: number): Promise<boolean> => {
    try {
      await api.deleteUser(id);
      setUsers(prev => prev.filter(user => user.id !== id));
      return true;
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to delete user');
      return false;
    }
  };
  return {
    users,
    loading,
    error,
    createUser,
    updateUser,
    deleteUser,
    refetch: () => fetchUsers()
  };
}
export function useUser(id: number) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  useEffect(() => {
    async function fetchUser() {
      try {
        setLoading(true);
        const userData = await api.getUserById(id);
        setUser(userData);
        setError(null);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Failed to fetch user');
        setUser(null);
      } finally {
        setLoading(false);
      }
    }
    if (id) {
      fetchUser();
    }
  }, [id]);
  return { user, loading, error };
}
Using Hooks in Components
// src/components/UserList.tsx
import React from 'react';
import { useUsers } from '../hooks/useApi';
export function UserList() {
  const { users, loading, error, createUser, deleteUser } = useUsers();
  if (loading) return <div>Loading users...</div>;
  if (error) return <div>Error: {error}</div>;
  const handleCreateUser = async () => {
    const newUser = await createUser({
      name: 'New User',
      email: 'new.user@example.com'
    });
    
    if (newUser) {
      console.log('User created:', newUser);
    }
  };
  return (
    <div>
      <h2>Users</h2>
      <button onClick={handleCreateUser}>Add User</button>
      
      <ul>
        {users.map(user => (
          <li key={user.id}>
            <span>{user.name} - {user.email}</span>
            <button onClick={() => deleteUser(user.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}
React Query Integration
For advanced data fetching with caching and synchronization:
Setup
npm install @tanstack/react-query
// src/lib/queryClient.ts
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      cacheTime: 10 * 60 * 1000, // 10 minutes
    },
  },
});
// src/App.tsx
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from './lib/queryClient';
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* Your app components */}
    </QueryClientProvider>
  );
}
React Query Hooks
// src/hooks/useApiQuery.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '../lib/api/config';
import type { User, CreateUserRequest } from '../types/api';
// Query hooks
export function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: () => api.getUsers(),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}
export function useUser(id: number) {
  return useQuery({
    queryKey: ['users', id],
    queryFn: () => api.getUserById(id),
    enabled: !!id, // Only fetch if ID is provided
  });
}
// Mutation hooks
export function useCreateUser() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: (userData: CreateUserRequest) => api.createUser(userData),
    onSuccess: (newUser) => {
      // Update users list cache
      queryClient.setQueryData(['users'], (old: User[] = []) => [...old, newUser]);
      // Invalidate users query to refetch
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}
export function useUpdateUser() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: ({ id, data }: { id: number; data: Partial<User> }) => 
      api.updateUser(id, data),
    onSuccess: (updatedUser) => {
      // Update individual user cache
      queryClient.setQueryData(['users', updatedUser.id], updatedUser);
      // Update users list cache
      queryClient.setQueryData(['users'], (old: User[] = []) =>
        old.map(user => user.id === updatedUser.id ? updatedUser : user)
      );
    },
  });
}
export function useDeleteUser() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: (id: number) => api.deleteUser(id),
    onSuccess: (_, deletedId) => {
      // Remove from users list cache
      queryClient.setQueryData(['users'], (old: User[] = []) =>
        old.filter(user => user.id !== deletedId)
      );
      // Remove individual user cache
      queryClient.removeQueries({ queryKey: ['users', deletedId] });
    },
  });
}
Using React Query Hooks
// src/components/UserListWithQuery.tsx
import React from 'react';
import { useUsers, useCreateUser, useDeleteUser } from '../hooks/useApiQuery';
export function UserListWithQuery() {
  const { data: users, isLoading, error } = useUsers();
  const createUser = useCreateUser();
  const deleteUser = useDeleteUser();
  if (isLoading) return <div>Loading users...</div>;
  if (error) return <div>Error: {error.message}</div>;
  const handleCreateUser = () => {
    createUser.mutate({
      name: 'New User',
      email: 'new.user@example.com'
    });
  };
  return (
    <div>
      <h2>Users</h2>
      <button 
        onClick={handleCreateUser} 
        disabled={createUser.isPending}
      >
        {createUser.isPending ? 'Creating...' : 'Add User'}
      </button>
      
      <ul>
        {users?.map(user => (
          <li key={user.id}>
            <span>{user.name} - {user.email}</span>
            <button 
              onClick={() => deleteUser.mutate(user.id)}
              disabled={deleteUser.isPending}
            >
              {deleteUser.isPending ? 'Deleting...' : 'Delete'}
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}
Next.js Specific Integration
API Routes with SpecJet Types
Use SpecJet types in Next.js API routes:
// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import type { User, CreateUserRequest } from '../../src/types/api';
import { api } from '../../src/lib/api/config';
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<User[] | User | { error: string }>
) {
  try {
    if (req.method === 'GET') {
      // Proxy to real API or return mock data
      const users = await api.getUsers();
      res.status(200).json(users);
    } else if (req.method === 'POST') {
      const userData: CreateUserRequest = req.body;
      const newUser = await api.createUser(userData);
      res.status(201).json(newUser);
    } else {
      res.setHeader('Allow', ['GET', 'POST']);
      res.status(405).json({ error: 'Method not allowed' });
    }
  } catch (error) {
    console.error('API Error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
}
Server-Side Rendering (SSR)
// pages/users.tsx
import { GetServerSideProps } from 'next';
import { api } from '../src/lib/api/config';
import type { User } from '../src/types/api';
interface Props {
  users: User[];
}
export default function UsersPage({ users }: Props) {
  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}
export const getServerSideProps: GetServerSideProps<Props> = async () => {
  try {
    // Use mock server during development
    const apiUrl = process.env.NODE_ENV === 'development' 
      ? 'http://localhost:3001'
      : process.env.API_URL;
      
    const apiClient = new ApiClient(apiUrl);
    const users = await apiClient.getUsers();
    
    return {
      props: {
        users,
      },
    };
  } catch (error) {
    console.error('Failed to fetch users:', error);
    return {
      props: {
        users: [],
      },
    };
  }
};
Static Site Generation (SSG)
// pages/users/[id].tsx
import { GetStaticPaths, GetStaticProps } from 'next';
import { api } from '../../src/lib/api/config';
import type { User } from '../../src/types/api';
interface Props {
  user: User;
}
export default function UserPage({ user }: Props) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
      <p>Status: {user.isActive ? 'Active' : 'Inactive'}</p>
    </div>
  );
}
export const getStaticPaths: GetStaticPaths = async () => {
  const users = await api.getUsers();
  const paths = users.map(user => ({
    params: { id: user.id.toString() },
  }));
  return {
    paths,
    fallback: 'blocking',
  };
};
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
  try {
    const id = Number(params?.id);
    const user = await api.getUserById(id);
    
    return {
      props: {
        user,
      },
      revalidate: 60, // Revalidate every 60 seconds
    };
  } catch (error) {
    return {
      notFound: true,
    };
  }
};
Authentication Integration
JWT Token Management
// src/hooks/useAuth.ts
import { useState, useEffect, createContext, useContext } from 'react';
import { api } from '../lib/api/config';
interface AuthState {
  token: string | null;
  user: User | null;
  isAuthenticated: boolean;
}
interface AuthContextType extends AuthState {
  login: (token: string) => void;
  logout: () => void;
}
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [authState, setAuthState] = useState<AuthState>({
    token: null,
    user: null,
    isAuthenticated: false,
  });
  useEffect(() => {
    // Load token from localStorage on mount
    const token = localStorage.getItem('auth_token');
    if (token) {
      api.setBearerToken(token);
      setAuthState(prev => ({
        ...prev,
        token,
        isAuthenticated: true,
      }));
      
      // Optionally fetch user info
      fetchCurrentUser();
    }
  }, []);
  const login = (token: string) => {
    localStorage.setItem('auth_token', token);
    api.setBearerToken(token);
    setAuthState(prev => ({
      ...prev,
      token,
      isAuthenticated: true,
    }));
    fetchCurrentUser();
  };
  const logout = () => {
    localStorage.removeItem('auth_token');
    api.setAuth(null);
    setAuthState({
      token: null,
      user: null,
      isAuthenticated: false,
    });
  };
  const fetchCurrentUser = async () => {
    try {
      const user = await api.getCurrentUser(); // Assuming this endpoint exists
      setAuthState(prev => ({ ...prev, user }));
    } catch (error) {
      console.error('Failed to fetch current user:', error);
      logout(); // Invalid token
    }
  };
  return (
    <AuthContext.Provider value=>
      {children}
    </AuthContext.Provider>
  );
}
export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}
Protected Routes
// src/components/ProtectedRoute.tsx
import { useAuth } from '../hooks/useAuth';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
interface Props {
  children: React.ReactNode;
}
export function ProtectedRoute({ children }: Props) {
  const { isAuthenticated } = useAuth();
  const router = useRouter();
  useEffect(() => {
    if (!isAuthenticated) {
      router.push('/login');
    }
  }, [isAuthenticated, router]);
  if (!isAuthenticated) {
    return <div>Redirecting to login...</div>;
  }
  return <>{children}</>;
}
Form Integration
Form with SpecJet Types
// src/components/UserForm.tsx
import React, { useState } from 'react';
import type { CreateUserRequest, User } from '../types/api';
import { useCreateUser } from '../hooks/useApiQuery';
interface Props {
  onSuccess?: (user: User) => void;
}
export function UserForm({ onSuccess }: Props) {
  const [formData, setFormData] = useState<CreateUserRequest>({
    name: '',
    email: '',
  });
  
  const createUser = useCreateUser();
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    createUser.mutate(formData, {
      onSuccess: (newUser) => {
        setFormData({ name: '', email: '' }); // Reset form
        onSuccess?.(newUser);
      },
    });
  };
  const handleChange = (field: keyof CreateUserRequest) => 
    (e: React.ChangeEvent<HTMLInputElement>) => {
      setFormData(prev => ({
        ...prev,
        [field]: e.target.value,
      }));
    };
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Name:</label>
        <input
          id="name"
          type="text"
          value={formData.name}
          onChange={handleChange('name')}
          required
        />
      </div>
      
      <div>
        <label htmlFor="email">Email:</label>
        <input
          id="email"
          type="email"
          value={formData.email}
          onChange={handleChange('email')}
          required
        />
      </div>
      
      <button type="submit" disabled={createUser.isPending}>
        {createUser.isPending ? 'Creating...' : 'Create User'}
      </button>
      
      {createUser.error && (
        <div style=>
          Error: {createUser.error.message}
        </div>
      )}
    </form>
  );
}
Development Workflow
Concurrent Development
Set up your development environment for optimal workflow:
{
  "scripts": {
    "dev": "next dev",
    "api:generate": "specjet generate",
    "api:watch": "specjet generate --watch",
    "api:mock": "specjet mock",
    "dev:full": "concurrently \"npm run api:mock\" \"npm run api:watch\" \"npm run dev\"",
    "build": "npm run api:generate && next build"
  }
}
Development Process
- Design API Contract: Edit 
api-contract.yaml - Generate Types: Run 
npm run api:watch(auto-regenerates on changes) - Start Mock Server: Run 
npm run api:mock - Develop Frontend: Run 
npm run dev - Test Integration: Use generated types and mock server
 
TypeScript Integration
Ensure TypeScript includes generated files:
// tsconfig.json
{
  "compilerOptions": {
    // ... other options
    "typeRoots": ["./node_modules/@types", "./src/types"]
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    "src/types/**/*",
    "src/api/**/*"
  ]
}
Testing
Testing with Mock Server
// src/__tests__/api.test.ts
import { api } from '../lib/api/config';
// Use mock server for testing
beforeAll(() => {
  api.setBaseUrl('http://localhost:3001');
});
describe('API Integration', () => {
  test('should fetch users', async () => {
    const users = await api.getUsers();
    expect(Array.isArray(users)).toBe(true);
    expect(users.length).toBeGreaterThan(0);
  });
  test('should create user', async () => {
    const newUser = await api.createUser({
      name: 'Test User',
      email: 'test@example.com',
    });
    
    expect(newUser).toHaveProperty('id');
    expect(newUser.name).toBe('Test User');
    expect(newUser.email).toBe('test@example.com');
  });
});
Component Testing
// src/__tests__/UserList.test.tsx
import { render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { UserListWithQuery } from '../components/UserListWithQuery';
const createTestQueryClient = () => new QueryClient({
  defaultOptions: {
    queries: { retry: false },
    mutations: { retry: false },
  },
});
describe('UserList', () => {
  test('renders users', async () => {
    const queryClient = createTestQueryClient();
    
    render(
      <QueryClientProvider client={queryClient}>
        <UserListWithQuery />
      </QueryClientProvider>
    );
    expect(screen.getByText('Loading users...')).toBeInTheDocument();
    
    // Wait for users to load from mock server
    await screen.findByText(/Users/);
  });
});
Troubleshooting
Common Issues
1. CORS Errors
# Enable CORS in mock server
npm run api:mock
# Or configure in specjet.config.js
export default {
  mock: {
    cors: {
      origin: ['http://localhost:3000'],
      credentials: true
    }
  }
};
2. TypeScript Errors
# Regenerate types
npm run api:generate
# Check TypeScript configuration
npx tsc --noEmit
3. Mock Server Connection
# Check if mock server is running
curl http://localhost:3001/users
# Check network tab in browser devtools
4. Build Issues
# Generate types before build
npm run api:generate
npm run build
Best Practices
- Always generate types before development
 - Use environment variables for API URLs
 - Implement proper error handling
 - Use React Query for complex data fetching
 - Test with mock server during development
 - Keep API client configuration centralized
 - Use TypeScript strict mode for better type safety
 
Next Steps
- Vue.js Integration: Similar patterns for Vue applications
 - Node.js Integration: Backend integration patterns
 - Best Practices: OpenAPI design recommendations
 - Configuration: Advanced configuration options