Building Admin panel builder comparison with Retool and A...
This guide provides a structured approach to building internal tools with a focus on balancing custom development and low-code solutions. Key considerations include data integration, access control, and maintainability for startup-scale applications.
Define data model and API requirements
Map internal tool workflows to database schemas. Identify required APIs (internal/external) and data transformations. Use Prisma schema or Supabase database editor to create models.
model User {
id Int @id @default(autoincrement())
name String
role Role @relation(fields: [roleId], references: [id])
roleId Int
}
enum Role {
ADMIN
EDITOR
VIEWER
}Set up project structure
Create Next.js app with API routes. Configure authentication middleware. Initialize database client (Prisma or Supabase). Use shadcn/ui for component baseline.
npx create-next-app@latest internal-tools --typescript
npm install @prisma/client shadcn/uiImplement role-based access control
Create middleware to validate user roles against requested routes. Store permissions in database. Use React context for client-side access checks.
const middleware = (auth: AuthClient) => {
return async (req: NextRequest) => {
const user = await auth.getUser();
if (!user.roles.includes('ADMIN')) {
return NextResponse.redirect(new URL('/unauthorized', req.url));
}
return NextResponse.next();
};
};⚠ Common Pitfalls
- •Missing server-side validation for critical operations
- •Inconsistent role state between client and server
Build data table components
Create reusable table components with sorting, filtering, and inline editing. Use server-side pagination for large datasets. Implement column configuration via JSON schema.
const DataGrid = ({ columns, fetchData }) => {
const [sort, setSort] = useState({ field: '', direction: 'asc' });
const data = useQuery(['data', sort], () => fetchData(sort));
return (
<table>
<thead>
{columns.map(col => (
<th key={col.key} onClick={() => setSort({ field: col.key, direction: ... })}>
{col.label}
</th>
))}
</thead>
<tbody>{data.map(row => <tr>{columns.map(col => <td>{row[col.key]}</td>)}</tr>)}</tbody>
</table>
);
};Connect multiple data sources
Create API proxies for external services. Use unified data transformation layer. Implement error handling for API failures and rate limiting.
const fetchExternalData = async (endpoint: string) => {
try {
const response = await fetch(`https://api.example.com/${endpoint}`, {
headers: { Authorization: `Bearer ${process.env.EXTERNAL_API_KEY}` },
});
return await response.json();
} catch (error) {
logError('External API failure', error);
throw new Error('Data unavailable');
}
};⚠ Common Pitfalls
- •Hardcoded API keys in client code
- •Insufficient retry strategies for transient failures
Add audit logging
Track user actions with timestamps and IP addresses. Store logs in separate database table. Implement log retention policy.
const logAction = async (userId: number, action: string, details: any) => {
await prisma.auditLog.create({
data: {
userId,
action,
ip: getIPAddress(),
timestamp: new Date(),
details: JSON.stringify(details)
}
});
};What you built
This implementation sequence addresses core internal tool requirements while maintaining security and scalability. Validate against actual workflow needs and adjust data modeling based on API response patterns and access frequency.