Guides

Full-Stack App Tutorial

Learn how to create a complete TypeScript application with authentication, data management, and RESTful APIs using AntelopeJS.
This tutorial covers many concepts from different AntelopeJS interfaces. If you encounter terms or methods you don't understand, please refer to the corresponding interface documentation for detailed explanations.

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:

package.json
"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
The 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

src/db/tables/user.table.ts
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;
}
Notice these key concepts:
  • @RegisterTable registers the table with `users' name into the default schema
  • Table.with(HashModifier) extension enables automatic password hashing and @Hashed enable it on the field
  • @Index defines indexable fields

Task table

src/db/tables/task.table.ts
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:

src/db/tables/index.ts
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

src/db/models/user.model.ts
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

src/db/models/task.model.ts
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:

src/db/models/index.ts
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:

src/routes/auth.ts
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:

src/routes/index.ts
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:

src/data-api/tasks.ts
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:

src/data-api/index.ts
export * from './tasks';

Application entry point

Finally, tie everything together in your main application file:

src/index.ts
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
If you need more information about AntelopeJS projects and how they work with modules, please refer to the Project chapter in the Get Started documentation.

Available endpoints

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.

For a quick start, you can select the "sample typescript" template when initializing a new module with the CLI:
ajs module init my-app-name
This will set up a project with all the features described in this tutorial.

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