Development
Local development guide for the RawStack Admin component.
Running the Admin locally
The Admin is built primarily with Vite for its frontend development workflow, local server, and build pipeline. During development, Vite serves the app locally with hot module replacement, while the Admin talks to the API through the configured VITE_API_URL.
If you are new to the toolchain or want to understand how the local dev server and build setup work, the official Vite documentation is worth reading before you customize the Admin further.
To get the Admin running locally, follow these steps:
cd apps/admin
npm install
cp .env.dist .env
npm run devThe Admin is then available at the local Vite dev server URL shown in the terminal, typically http://localhost:5173.
Environment configuration
Vite exposes environment variables prefixed with VITE_ to the client bundle. For local development, point the Admin at the local API.
The quick start uses:
VITE_API_URL=http://localhost:3001/v1Data fetching and UI models
The Admin uses @rawstack/api-client to talk to the API. That gives the frontend a type-safe way to call backend endpoints and keeps it aligned with the API contract.
That said, we do not want the UI layer tightly coupled to the API client itself.
For example, the API client may expose a User type, but UI components do not consume that type directly. Instead, the frontend defines its own UserModel for use in the interface. The data fetching layer is responsible for calling the API, transforming the response into a UserModel, and passing that model into the UI.
This separation keeps components simpler and makes the app easier to evolve. A component only needs to know that a hook returns a user model; it does not need to know whether that data came from React Query, the API client, caching, or any other implementation detail.
For example, here is the UserModel used by the UI:
import dayjs, { Dayjs } from 'dayjs';
import { User } from '@rawstack/api-client';
export default class UserModel {
public readonly dateCreated: Dayjs;
public readonly dateUpdated: Dayjs;
constructor(
public readonly id: string,
public readonly email: string,
public readonly roles: string[],
dateCreated: string,
dateUpdated: string,
public readonly unverifiedEmail?: string,
) {
this.dateCreated = dayjs(dateCreated);
this.dateUpdated = dayjs(dateUpdated);
}
get isAdmin(): boolean {
return this.roles.includes('ADMIN');
}
get isVerified(): boolean {
return this.roles.includes('VERIFIED_USER');
}
static createFromApiUser(user: User): UserModel {
return new UserModel(user.id, user.email, user.roles, user.createdAt, user.updatedAt, user.unverifiedEmail);
}
}Here is a hook that fetches user data and transforms it into a UserModel:
import { useQuery } from '@tanstack/react-query';
import Api from '@/lib/api/api.ts';
import UserModel from '@/lib/model/user-model.ts';
import { useAuth } from '@/lib/context/auth-context.tsx';
interface UseGetUserParams {
userId?: string;
}
export function useGetUser({ userId }: UseGetUserParams) {
const { user: currentUser } = useAuth();
const { data, isPending } = useQuery({
queryKey: ['user', `${userId === currentUser?.id ? 'current' : userId}`],
queryFn: () => {
if (!userId) {
return Promise.reject('User ID not set');
}
return Api.user.getUser(userId);
},
select: (data) => {
return UserModel.createFromApiUser(data.data.item);
},
enabled: !!userId,
});
return { user: data, isBusy: isPending };
}And here is a small UI component that uses useGetUser. Because the hook returns a UserModel, the component can stay focused on rendering rather than on API details:
import { useGetUser } from '@/hooks/user/use-get-user.ts';
interface UserSummaryProps {
userId: string;
}
export function UserSummary({ userId }: UserSummaryProps) {
const { user, isBusy } = useGetUser({ userId });
if (isBusy) {
return <p>Loading user...</p>;
}
if (!user) {
return <p>User not found.</p>;
}
return (
<div className="rounded-md border p-4">
<p className="text-sm text-muted-foreground">Signed in as</p>
<p className="font-medium">{user.email}</p>
</div>
);
}This pattern keeps API concerns in the data layer and presentation concerns in the component layer. In practice, that makes the Admin easier to understand, test, and customize.
Project structure
The Admin follows a standard Vite and React project structure.
src/main.tsx: application entry pointsrc/App.tsx: root component with router and shared providerssrc/routes: route-level componentssrc/components: shared UI componentssrc/lib: API helpers, hooks, and utilitiessrc/assets: images and other bundled assetspublic: static files copied directly intodist
Additional implementation details
API client
The @rawstack/api-client package is currently consumed as a local package:
{
"@rawstack/api-client": "file:./packages/api-client"
}This keeps the wider RawStack codebase easy to distribute and work on locally. In a larger or more independently deployed setup, you may prefer to publish this package to GitHub Packages or npm and consume it as a normal versioned dependency.
Path alias
@/* is aliased to src/ in both vite.config.ts and tsconfig.json, making imports easier to read and maintain than long relative paths.
Sharing UI models
UserModel is an example of a UI-facing model that could be shared across the Admin, Web, and mobile apps in the RawStack codebase.
In this example, the models are duplicated within each app to keep the structure simple and easy to follow. In a larger production codebase, you may want to move shared models into a separate package so they can be reused across clients while still keeping API concerns isolated from the UI.
Useful scripts
npm run dev: start the Vite dev servernpm run build: run the TypeScript check and create a production buildnpm run preview: preview the production build locallynpm run lint: run ESLintnpm run test: run Vitest in watch modenpm run test:ui: run Vitest with its browser UInpm run test:e2e: run Playwright end-to-end tests