A fullstack admin panel application with SvelteKit 5 frontend and ElysiaJS backend.
elysian-realm/
├── client/ # SvelteKit 5 frontend
│ ├── src/
│ │ ├── lib/ # Shared components, utilities, and types
│ │ │ ├── assets/ # Static assets like images and icons
│ │ │ ├── components/ # Reusable UI components
│ │ │ ├── datasource/ # Data fetching utilities
│ │ │ ├── state/ # Application state management
│ │ │ ├── types/ # TypeScript types
│ │ │ └── utils/ # Utility functions
│ │ ├── routes/ # SvelteKit routes with file-based routing
│ │ ├── app.css # Global CSS with TailwindCSS
│ │ ├── app.html # HTML entry point
│ │ └── hooks/
│ ├── static/ # Static assets
│ ├── svelte.config.js # SvelteKit configuration
│ ├── vite.config.ts # Vite configuration
│ ├── tailwind.config.js # TailwindCSS configuration
│ └── package.json
├── src/ # Elysia backend
│ ├── routes/
│ ├── controllers/
│ ├── middleware/
│ ├── plugins/
│ ├── types/
│ ├── prisma/
│ ├── utils/
│ ├── config/
│ └── index.ts
├── prisma/ # Prisma schema
│ ├── schema.prisma
│ └── seed.ts
├── package.json
├── bun.lock
└── README.md
You can run this application in two ways: using Docker (recommended) or manually installing dependencies.
docker-compose up --build
This will start all services:
For development with hot reloading:
docker-compose -f docker-compose.dev.yml up --build
bun install
.env.example file to .env:cp .env.example .env
.env file with your actual configurationelysian-realmDATABASE_URL in the .env file with your database credentials:DATABASE_URL="postgresql://username:password@localhost:5432/elysian-realm?schema=public"
bunx prisma migrate dev --name init
bunx prisma db seed
bun run src/index.ts
cd client
bun install
bun run dev
bun run src/index.ts - Start the development serverbun run test - Run the test suitebunx prisma migrate dev - Run database migrationsbunx prisma db seed - Seed the databasebunx prisma studio - Open Prisma Studiobun run dev - Start the development serverbun run build - Build for productionbun run preview - Preview the production builddocker-compose up --build - Start all services in production modedocker-compose -f docker-compose.dev.yml up --build - Start all services in development mode with hot reloadingdocker-compose down - Stop all servicesdocker-compose logs - View logs from all servicesThis project uses Bun for testing. To run tests:
bun run test # Run all tests once
bun run test:watch # Run tests in watch mode
Note: This project uses Bun's built-in test runner. Both bun test and bun run test will work, but bun test is the direct way to run tests using Bun's native testing capabilities.
Create a .env file in the root directory based on the .env.example template:
cp .env.example .env
Then update the values in the .env file with your actual configuration:
DATABASE_URL="postgresql://username:password@localhost:5432/elysian-realm?schema=public"
JWT_SECRET="your-super-secret-jwt-key"
PORT=3000
The application now uses a centralized configuration approach. Instead of directly accessing process.env, configuration values are accessed through the config object imported from src/config/config.ts. This provides better type safety and default values.
After seeding the database, you can log in with:
[email protected]password (hashed in the seed file)/api/auth/login - User login/api/auth/register - User registration/api/admin/users - List all users/api/admin/users/:id - Get user details/api/admin/users/:id - Update user/api/admin/users/:id - Delete user/api/admin/roles - List all roles/api/admin/roles - Create new role/api/admin/roles/:id - Update role/api/admin/roles/:id - Delete role/api/admin/permissions - List all permissions/api/admin/permissions - Create new permissionThis project includes a complete Docker setup with four services:
The production Docker images are optimized for size and security:
This project includes full TypeScript type safety for:
The types are automatically generated from the Prisma schema and manually defined for:
This ensures proper type inference throughout the application, even when using the JWT plugin across different modules.
This project uses Data Transfer Objects (DTOs) for defining API response structures and validation schemas. The base DTOs are defined in src/dto/base.dto.ts and provide common response formats and pagination structures.
All API responses follow a consistent structure with a meta field containing status information and a data field containing the actual response data:
{
meta: {
code: string, // Response status code (e.g., 'AUTH-200', 'ADMIN-404')
message: string, // Human-readable response message
errors?: Array<{ // Optional validation errors
field: string, // Field that caused the error
messages: string[] // Array of error messages for the field
}>
},
data: T // Generic data payload
}
Paginated responses follow a standard format with page information and an array of items:
{
page: number, // Current page number
limit: number, // Number of items per page
total: number, // Total number of items
pages: number, // Total number of pages
data: T[] // Array of items for the current page
}
Pagination can be controlled via query parameters:
page: Page number to retrieve (default: 1)limit: Number of items per page (default: 10)This project follows a comprehensive testing strategy with a focus on testability, dependency injection, and proper isolation. Below are the guidelines and best practices for writing unit tests in this codebase.
Components (controllers, services, middleware) should be designed using the Builder pattern to enable easy dependency injection and replacement:
// Example controller with dependency injection
interface AdminControllerOptions {
service?: AdminService
adminMiddleware?: ReturnType<typeof adminMiddleware>
auditMiddleware?: ReturnType<typeof auditMiddleware>
}
export const createAdminController = (options: AdminControllerOptions = {}) => {
const service = options.service || adminService
const admin = options.adminMiddleware || adminMiddleware()
const audit = options.auditMiddleware || auditMiddleware()
return new Elysia({ name: 'admin-controller' })
.use(responsePlugin({ defaultServiceName: 'ADMIN' }))
.group('/api/admin', (app) =>
app
.use(admin)
.use(audit)
// Controller routes here...
)
}
This pattern enables:
Each module should have corresponding test files:
<component>.test.ts - Endpoint/integration tests<component>.mock.test.ts - Unit tests with mocked dependenciesUse factory functions or direct object mocking for services:
// Create mock service
const mockAdminService = new AdminService(mockPrisma)
// Override specific methods
const originalMethod = mockAdminService.getUsers
mockAdminService.getUsers = mock(() => Promise.resolve(mockResult)) as any
// Remember to restore after test
mockAdminService.getUsers = originalMethod
Create lightweight mock middleware that bypasses actual authentication but provides test contexts:
const mockAuthMiddleware = (app: Elysia) => {
return app.derive(() => ({
user: {
id: '1',
email: '[email protected]',
name: 'Test User',
role_id: '1',
role: {
id: '1',
name: 'admin',
permissions: ['admins.read', 'admins.create']
}
}
}))
}
Create mock plugins that return properly structured Elysia plugins:
const createMockAdminAccessTokenPlugin = () => {
return (app: Elysia) => app.derive(() => ({
adminAccessToken: mockAdminAccessToken
}))
}
Follow the AAA pattern (Arrange, Act, Assert):
it('should get admins list successfully', async () => {
// Arrange - Setup test data and mocks
const originalGetUsers = mockAdminService.getUsers
const mockResult = { /* mock data */ }
mockAdminService.getUsers = mock(() => Promise.resolve(mockResult)) as any
// Act - Execute the test
const app = new Elysia()
.use(createAdminController({
service: mockAdminService,
adminMiddleware: adminMiddleware({auth: mockAuthMiddleware as any}),
auditMiddleware: mockAuditMiddleware as any,
}))
const response = await app.handle(
new Request('http://localhost/api/admin/admins', {
method: 'GET'
})
)
// Assert - Verify results
expect(response.status).toBe(200)
const body = await response.json()
expect(body.meta.code).toBe('ADMIN-200')
// Restore original method
mockAdminService.getUsers = originalGetUsers
})
Always inject dependencies to enable mocking:
const app = new Elysia()
.use(createAdminController({
service: mockAdminService, // Inject mock service
adminMiddleware: mockAuthMiddleware as any, // Inject mock auth
auditMiddleware: mockAuditMiddleware as any, // Inject mock audit
}))
Ensure tests don't interfere with each other by:
Controllers should be tested for:
Services should be tested for:
Middleware should be tested for:
Mock at the Right Level
Test Realistic Scenarios
Keep Tests Independent
Use Descriptive Test Names
// Good
it('should return 401 for invalid credentials', async () => { ... })
// Bad
it('should fail', async () => { ... })
Focus on Behavior, Not Implementation
Proper Test Data Management
it('should perform operation successfully', async () => {
// Mock service to return success
mockService.method = mock(() => Promise.resolve(successResult))
// Execute request
const response = await app.handle(request)
// Verify success response
expect(response.status).toBe(200)
// ... additional assertions
})
it('should handle error conditions gracefully', async () => {
// Mock service to return error
mockService.method = mock(() => Promise.resolve({ error: 'Error message' }))
// Execute request
const response = await app.handle(request)
// Verify error response
expect(response.status).toBe(400) // or appropriate error status
// ... additional assertions
})
it('should deny access when user lacks required permission', async () => {
// Create mock auth middleware with limited permissions
const mockAuthWithLimitedPermissions = (app: Elysia) => {
return app.derive(() => ({
user: { /* user with limited permissions */ }
}))
}
// Execute request with limited permissions
const response = await app.handle(request)
// Verify access denied
expect(response.status).toBe(403)
})
# Run all tests once
bun test
# Run tests in watch mode
bun test --watch
# Run specific test files
bun test src/modules/admin/controller/admin_controller.mock.test.ts
Note: This project uses Bun's built-in test runner. You can use bun test directly to run tests using Bun's native testing capabilities.