Full-Stack App Tutorial
App structure
A typical AntelopeJS application has the following structure:
src/
├── index.ts // Application entry point
├── db/ // Database layer
│ ├── tables/ // Table definitions
│ ├── models/ // Data models with business logic
├── routes/ // API routes and controllers
└── data-api/ // Data API controllers
Setting up the module
Initialize a new AntelopeJS module
ajs module init my-module-name
When prompted, select the blank template.
Configure your package.json
Your package.json should include these AntelopeJS interfaces:
"antelopeJs": {
"imports": [
"core@beta",
"database@beta",
"database-decorators@beta",
"api@beta",
"auth@beta",
"data-api@beta"
],
"baseUrl": "dist",
"paths": {
"@/*": [
"*"
]
}
}
You can also add interfaces using the CLI instead of manually editing package.json:
ajs module imports add core@beta database@beta database-decorators@beta api@beta auth@beta data-api@beta
ajs
command requires the AntelopeJS CLI to be installed globally. See the CLI Introduction for installation instructions.Defining database tables
First, let's define our database tables using decorators. We'll create two tables: User and Task.
User table
import { Table, Index, HashModifier, Hashed, RegisterTable } from '@ajs/database-decorators/beta';
/**
* User table definition with basic user fields
*/
@RegisterTable('users', 'default')
export class User extends Table.with(HashModifier) {
@Index({ primary: true })
declare _id: string;
@Index()
declare email: string;
@Hashed()
declare password: string;
declare createdAt: Date;
declare updatedAt: Date;
}
@RegisterTable
registers the table with `users' name into the default schemaTable.with(HashModifier)
extension enables automatic password hashing and@Hashed
enable it on the field@Index
defines indexable fields
Task table
import { Table, Index, RegisterTable } from '@ajs/database-decorators/beta';
/**
* Task table definition with title, description and userId fields
*/
@RegisterTable('tasks', 'default')
export class Task extends Table {
@Index({ primary: true })
declare _id: string;
declare title: string;
declare description: string;
@Index()
declare userId: string;
declare createdAt: Date;
declare updatedAt: Date;
}
Registering tables
Create an index file to export your tables:
export * from './user.table';
export * from './task.table';
Creating data models
Data models add business logic to your tables. Let's create models for User and Task.
User model
import { BasicDataModel, GetModel } from '@ajs/database-decorators/beta';
import { User } from '../tables/user.table';
// Create a basic model for the User table
const UserModelBase = BasicDataModel(User, 'users');
/**
* Extended User Model with additional custom methods
*/
export class UserModel extends UserModelBase {
/**
* Get user by email
*/
async getUserByEmail(email: string) {
return this.getBy('email', email).then((users) => (users.length > 0 ? users[0] : undefined));
}
/**
* Create a new user
*/
async createUser(userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>) {
const now = new Date();
await this.insert({
...userData,
createdAt: now,
updatedAt: now,
});
}
/**
* Update user data
*/
async updateUser(id: string, userData: Partial<Omit<User, 'id' | 'createdAt'>>) {
const updateData = {
...userData,
updatedAt: new Date(),
};
return this.update(id, updateData);
}
}
/**
* Get or create a UserModel instance
*/
export function getUserModel(databaseName: string = 'main') {
return GetModel(UserModel, databaseName);
}
Task model
import { BasicDataModel, GetModel } from '@ajs/database-decorators/beta';
import { Task } from '../tables/task.table';
// Create a basic model for the Task table
const TaskModelBase = BasicDataModel(Task, 'tasks');
/**
* Extended Task Model with additional custom methods
*/
export class TaskModel extends TaskModelBase {
/**
* Get tasks by user ID
*/
async getTasksByUserId(userId: string) {
return this.getBy('userId', userId);
}
/**
* Create a new task
*/
async createTask(taskData: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>) {
const now = new Date();
await this.insert({
...taskData,
createdAt: now,
updatedAt: now,
});
}
/**
* Update task data
*/
async updateTask(id: string, taskData: Partial<Omit<Task, 'id' | 'createdAt'>>) {
const updateData = {
...taskData,
updatedAt: new Date(),
};
return this.update(id, updateData);
}
}
/**
* Get or create a TaskModel instance
*/
export function getTaskModel(databaseName: string = 'main') {
return GetModel(TaskModel, databaseName);
}
Exporting models
Create an index file to register your models:
export * from './user.model';
export * from './task.model';
Creating authentication routes
AntelopeJS makes it easy to create authentication workflows. Let's set up registration, login, and user profile endpoints:
import { Controller, Post, Get, Context, RequestContext, HTTPResult, RawBody } from '@ajs/api/beta';
import { getUserModel } from '../db/models/user.model';
import { User } from '../db/tables/user.table';
import { SignRaw, Authentication } from '@ajs/auth/beta';
// Configuration
const TOKEN_EXPIRY = '24h';
// Define interfaces for request data
interface RegisterRequest {
email: string;
password: string;
}
interface LoginRequest {
email: string;
password: string;
}
// Define the JWT payload interface
interface AuthUser {
userId: string;
email: string;
}
/**
* Authentication Controller
*/
export class AuthController extends Controller('/auth') {
@Context()
declare context: RequestContext;
/**
* Register a new user
* POST /auth/register
*/
@Post('/register')
async register(@RawBody() body: Buffer) {
const data = JSON.parse(body.toString()) as RegisterRequest;
const { email, password } = data;
// Validation code...
const userModel = getUserModel();
// Check if user already exists
const existingUser = await userModel.getUserByEmail(email);
if (existingUser) {
return new HTTPResult(409, 'User with this email already exists');
}
// Create the user
await userModel.createUser({
email,
password,
} as Omit<User, 'id' | 'createdAt' | 'updatedAt'>);
}
/**
* Login user
* POST /auth/login
*/
@Post('/login')
async login(@RawBody() body: Buffer) {
const data = JSON.parse(body.toString()) as LoginRequest;
const { email, password } = data;
const userModel = getUserModel();
// Find user by email
const user = await userModel.getUserByEmail(email);
if (!user) {
return new HTTPResult(401, 'Invalid credentials');
}
// Compare password (automatically uses hashed comparison)
const isPasswordMatch = user.password === password;
if (!isPasswordMatch) {
return new HTTPResult(401, 'Invalid credentials');
}
// Generate JWT token
const token = await SignRaw(
{
userId: user._id,
email: user.email,
},
{ expiresIn: TOKEN_EXPIRY },
);
return {
token,
user: {
id: user._id,
email: user.email,
},
};
}
/**
* Get current user profile using Authentication decorator
* GET /auth/me
*/
@Get('/me')
async getUserProfile(@Authentication() auth: AuthUser) {
const userModel = getUserModel();
const user = await userModel.get(auth.userId);
if (!user) {
return new HTTPResult(404, 'User not found');
}
return {
id: user._id,
email: user.email,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
authenticated: true,
};
}
}
Registering controllers
Create an index file to register all controllers:
import { AuthController } from './auth';
// Register controllers
export const controllers = [AuthController];
Creating a data API
AntelopeJS provides a powerful Data API system that automatically creates CRUD endpoints for your models:
import { Controller } from '@ajs/api/beta';
import { DataController, DefaultRoutes, RegisterDataController } from '@ajs/data-api/beta';
import { Authentication } from '@ajs/auth/beta';
import { Task } from '../db/tables/task.table';
import { TaskModel } from '../db/models/task.model';
import { Access, AccessMode, Listable, Mandatory, ModelReference, Sortable } from '@ajs/data-api/beta/metadata';
import { StaticModel } from '@ajs/database-decorators/beta';
/**
* Custom route definition with authentication
*/
const AuthenticatedRoutes = {
get: {
...DefaultRoutes.Get,
args: [Authentication(), ...DefaultRoutes.Get.args],
},
list: {
...DefaultRoutes.List,
args: [Authentication(), ...DefaultRoutes.List.args],
},
new: {
...DefaultRoutes.New,
args: [Authentication(), ...DefaultRoutes.New.args],
},
edit: {
...DefaultRoutes.Edit,
args: [Authentication(), ...DefaultRoutes.Edit.args],
},
delete: {
...DefaultRoutes.Delete,
args: [Authentication(), ...DefaultRoutes.Delete.args],
},
};
/**
* Task Data API Controller
*/
@RegisterDataController()
export class TaskDataAPI extends DataController(Task, AuthenticatedRoutes, Controller('/tasks')) {
@ModelReference()
@StaticModel(TaskModel, 'default')
declare taskModel: TaskModel;
@Listable()
@Sortable()
@Access(AccessMode.ReadOnly)
declare _id: string;
@Listable()
@Sortable()
@Mandatory('new', 'edit')
@Access(AccessMode.ReadWrite)
declare title: string;
@Listable()
@Access(AccessMode.ReadWrite)
declare description: string;
@Listable()
@Sortable()
@Access(AccessMode.ReadOnly)
declare userId: string;
@Listable()
@Sortable()
@Access(AccessMode.ReadOnly)
declare createdAt: Date;
@Listable()
@Sortable()
@Access(AccessMode.ReadOnly)
declare updatedAt: Date;
}
Registering data API controllers
Create an index file to register your Data API controllers:
export * from './tasks';
Application entry point
Finally, tie everything together in your main application file:
import { InitializeDatabaseFromSchema } from '@ajs/database-decorators/beta';
import './db';
import './routes';
import './data-api';
export function construct(): void {}
export async function start(): Promise<void> {
await InitializeDatabaseFromSchema('default', 'default');
}
export function destroy(): void {}
export function stop(): void {}
Running your application
To run your application, you need to bundle your module into an AntelopeJS project:
# Initialize a new AntelopeJS project
ajs project init my-project-name
# When prompted if you have an app module, select "yes"
# This will add your module to your antelope.json
# Once project initialization is complete, run the following to configure dependencies
ajs project modules fix
# This command will help you add all required modules to your antelope.json
# It's needed because your app module imports interfaces that need to be implemented
# Run your project
ajs project run
# Or with watch mode for development
ajs project run -w
Available endpoints
POST /auth/register
- Register a new userPOST /auth/login
- Login and get a JWT tokenGET /auth/me
- Get current user profile (requires authentication)
GET /tasks
- List all tasks (requires authentication)GET /tasks/:id
- Get a specific task (requires authentication)POST /tasks
- Create a new task (requires authentication)PUT /tasks/:id
- Update a task (requires authentication)DELETE /tasks/:id
- Delete a task (requires authentication)
Conclusion
For the complete code example, check out the AntelopeJS TypeScript template on GitHub.
You can use this as a starting point for your own applications, extending it with additional features.
ajs module init my-app-name
You've now built a complete TypeScript application with AntelopeJS! This example demonstrates how to:
Define database tables
Create tables with decorators for structure and validation
Create data models
Add business logic to your database tables
Build authentication
Implement secure user registration and login
Create REST APIs
Automatically generate CRUD operations
Complete application
Tie everything together into a cohesive app