Feature Organization
Nexgent uses a feature-based organization pattern where code is grouped by domain feature rather than technical type.
Directory Structure
src/
├── app/ # Next.js App Router (pages)
├── features/ # Feature modules
├── infrastructure/ # API clients, auth, WebSocket
└── shared/ # Shared components, hooks, utilsFeatures Directory
Each feature is a self-contained module with its own components, hooks, types, and exports.
- index.ts
Feature Module Structure
Every feature follows the same structure:
features/{feature-name}/
├── components/ # Feature-specific components
│ └── {component-name}/
│ └── {component-name}.tsx
├── hooks/ # React Query hooks, custom hooks
│ └── use-{resource}.ts
├── types/ # TypeScript types
│ └── {feature}.types.ts
└── index.ts # Public exportsBarrel Exports
Each feature has an index.ts that exports its public API:
// features/agents/index.ts
export { AgentSwitcher } from './components/agent-switcher/agent-switcher';
export { CreateAgentDialog } from './components/create-agent-dialog/create-agent-dialog';
export { useAgents, useAgent, useCreateAgent, useUpdateAgent } from './hooks/use-agent';
export { useAgentTradingConfig } from './hooks/use-agent-trading-config';
export type { Agent, CreateAgentRequest } from './types/agent.types';This allows clean imports:
import { useAgents, AgentSwitcher, CreateAgentDialog } from '@/features/agents';Feature List
| Feature | Description |
|---|---|
| agents | Agent CRUD, trading config, performance |
| positions | Live positions table, close dialogs |
| trades | Historical swaps, trade detail |
| trading-signals | Signal history and detail |
| transactions | Transaction history |
| wallets | Wallet management, deposit/withdraw |
| auth | Login/register forms |
| api-keys | API key management |
| integrations | Data source connections |
| system-health | System health display |
Component Pattern
Components are organized in folders with their name:
components/
└── live-positions-table/
└── live-positions-table.tsxComponent Structure
// features/positions/components/live-positions-table/live-positions-table.tsx
'use client';
import { useWebSocket, type LivePosition } from '@/infrastructure/websocket';
import { useAgentSelection } from '@/shared/contexts/agent-selection.context';
import { Table, TableHeader, TableBody, TableRow, TableCell } from '@/shared/components/ui/table';
interface LivePositionsTableProps {
className?: string;
}
export function LivePositionsTable({ className }: LivePositionsTableProps) {
const { selectedAgentId } = useAgentSelection();
const { positions, isConnected } = useWebSocket(selectedAgentId);
if (!isConnected) {
return <ConnectionStatus />;
}
if (positions.length === 0) {
return <EmptyState />;
}
return (
<Table className={className}>
<TableHeader>
<TableRow>
<TableCell>Token</TableCell>
<TableCell>Amount</TableCell>
<TableCell>P/L</TableCell>
</TableRow>
</TableHeader>
<TableBody>
{positions.map(position => (
<PositionRow key={position.id} position={position} />
))}
</TableBody>
</Table>
);
}Hook Pattern
Feature hooks wrap React Query for data fetching:
// features/agents/hooks/use-agent.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { AgentsService } from '@/infrastructure/api/services/agents.service';
const agentsService = new AgentsService();
/**
* Hook to fetch all agents for a user
*/
export function useAgents(userId?: string) {
return useQuery({
queryKey: ['agents', userId],
queryFn: () => agentsService.getAgents(userId),
enabled: !!userId,
staleTime: 5 * 60 * 1000, // 5 minutes
refetchOnWindowFocus: false,
});
}
/**
* Hook to create a new agent
*/
export function useCreateAgent() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateAgentRequest) => agentsService.createAgent(data),
onSuccess: (newAgent) => {
// Optimistically update cache
queryClient.setQueryData(['agents', newAgent.userId], (old: Agent[] | undefined) => {
return old ? [...old, newAgent] : [newAgent];
});
// Invalidate to ensure fresh data
queryClient.invalidateQueries({ queryKey: ['agents'] });
},
});
}Hooks encapsulate React Query configuration, cache invalidation, and optimistic updates so components stay simple.
Infrastructure Layer
The infrastructure/ folder contains technical concerns:
infrastructure/
├── api/
│ ├── client/
│ │ └── api-client.ts # HTTP client
│ ├── services/
│ │ ├── agents.service.ts # Agent API calls
│ │ ├── wallets.service.ts # Wallet API calls
│ │ └── ...
│ └── hooks/
│ └── use-system-health.ts
├── auth/
│ ├── auth-config.ts # NextAuth config
│ └── token-utils.ts # Token handling
└── websocket/
├── hooks/
│ └── use-websocket.ts # WebSocket hook
└── types/
└── websocket.types.tsAPI Services
Services encapsulate API calls:
// infrastructure/api/services/agents.service.ts
import { apiClient } from '../client/api-client';
import { handleApiError } from '../client/error-handler';
export class AgentsService {
async getAgents(userId?: string): Promise<Agent[]> {
const response = await apiClient.get('/api/v1/agents');
if (!response.ok) {
throw await handleApiError(response);
}
return response.json();
}
async createAgent(data: CreateAgentRequest): Promise<Agent> {
const response = await apiClient.post('/api/v1/agents', data);
if (!response.ok) {
throw await handleApiError(response);
}
return response.json();
}
}Shared Layer
The shared/ folder contains reusable code:
shared/
├── components/
│ ├── ui/ # shadcn/ui components
│ ├── layout/ # Sidebar, navigation
│ ├── loading/ # Loading states
│ └── error/ # Error boundaries
├── contexts/
│ ├── agent-selection.context.tsx
│ ├── currency.context.tsx
│ ├── trading-mode.context.tsx
│ └── providers.tsx
├── hooks/
│ ├── use-auth.ts
│ └── use-toast.ts
├── utils/
│ ├── formatting.ts
│ └── error-handling.ts
└── types/
└── api.types.tsImport Aliases
Path aliases simplify imports:
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}// Clean imports
import { Button } from '@/shared/components/ui/button';
import { useAgents } from '@/features/agents';
import { apiClient } from '@/infrastructure/api/client/api-client';