Guides

Full-Stack Application

Build a complete task management API from scratch using database tables, authentication, and automatic CRUD endpoints in AntelopeJS.

Overview

This tutorial builds a task management API with user registration, login, and authenticated CRUD operations. By the end, you will have a running application with these endpoints:

  • POST /api/auth/register -- Create a new user account
  • POST /api/auth/login -- Authenticate and receive a token
  • GET /api/auth/profile -- Retrieve the authenticated user's profile
  • GET /api/tasks -- List all tasks (authenticated)
  • GET /api/tasks/:id -- Get a single task (authenticated)
  • POST /api/tasks -- Create a task (authenticated)
  • PUT /api/tasks/:id -- Update a task (authenticated)
  • DELETE /api/tasks/:id -- Delete a task (authenticated)

Prerequisites

This tutorial uses four interface packages. Install them all in your module.

npm install @antelopejs/interface-api @antelopejs/interface-database-decorators @antelopejs/interface-auth @antelopejs/interface-data-api

You also need the AntelopeJS CLI installed globally. Refer to the Installation guide if you have not set it up yet.

Application Structure

The completed project follows this layout:

my-app/
├── antelope.config.ts
├── src/
│   ├── index.ts
│   ├── db/
│   │   ├── tables/
│   │   │   ├── user.ts
│   │   │   └── task.ts
│   │   └── models/
│   │       ├── user-model.ts
│   │       └── task-model.ts
│   ├── routes/
│   │   └── auth.ts
│   └── data-api/
│       └── tasks.ts
└── package.json

The db/ directory holds table definitions and data models. The routes/ directory contains custom controllers for authentication. The data-api/ directory uses the Data API to generate CRUD endpoints automatically.

Building the Application

Set up the module

Create the module directory and initialize it with the required package configuration. The antelopeJs field in package.json tells the core how to load and resolve the module.

package.json
{
  "name": "my-app",
  "version": "1.0.0",
  "main": "dist/index.js",
  "antelopeJs": {
    "implements": [],
    "baseUrl": "dist",
    "paths": {
      "@/*": ["*"]
    },
    "moduleAliases": {},
    "defaultConfig": {}
  },
  "dependencies": {
    "@antelopejs/interface-api": "*",
    "@antelopejs/interface-auth": "*",
    "@antelopejs/interface-data-api": "*",
    "@antelopejs/interface-database-decorators": "*"
  }
}

Set up TypeScript configuration with decorator support:

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "strict": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Define the User table

The User table stores account credentials and profile information. The @Hashed modifier on the password property ensures passwords are hashed before storage. Extend Table.with(HashModifier) to enable hashing support.

src/db/tables/user.ts
import { Table, RegisterTable, Index, Hashed } from "@antelopejs/interface-database-decorators";
import { HashModifier } from "@antelopejs/interface-database-decorators";

@RegisterTable("users", "main")
class User extends Table.with(HashModifier) {
  @Index()
  declare email: string;

  @Hashed()
  declare password: string;

  declare name: string;
}

export { User };

The @Index decorator on email enables fast lookups when authenticating users. The @Hashed decorator ensures the plaintext password is never stored in the database.

Define the Task table

The Task table stores task records associated with users. The userId field links each task to its owner, and the @Index decorator enables efficient filtering by user.

src/db/tables/task.ts
import { Table, RegisterTable, Index } from "@antelopejs/interface-database-decorators";

@RegisterTable("tasks", "main")
class Task extends Table {
  @Index()
  declare userId: string;

  declare title: string;
  declare description: string;
  declare completed: boolean;
}

export { Task };

Create the User model

The User model extends BasicDataModel with custom methods for authentication workflows. The getUserByEmail method retrieves a user by email address, and createUser handles user registration.

src/db/models/user-model.ts
import { BasicDataModel, GetModel } from "@antelopejs/interface-database-decorators";
import { User } from "../tables/user";

class UserModel extends BasicDataModel(User) {
  async getUserByEmail(email: string) {
    const users = await this.table.filter({ email }).run();
    return users.length > 0 ? users[0] : null;
  }

  async createUser(data: { name: string; email: string; password: string }) {
    const existing = await this.getUserByEmail(data.email);
    if (existing) {
      throw new Error("A user with this email already exists");
    }
    return this.insert(data);
  }
}

export function getUserModel() {
  return GetModel(UserModel);
}

export { UserModel };

The BasicDataModel function provides get, getBy, getAll, insert, update, and delete methods automatically. Model instances require a database connection, so use GetModel(YourModel) to obtain a properly initialized instance. The custom methods add application-specific logic on top.

Create the Task model

The Task model adds a method to retrieve all tasks belonging to a specific user. The task endpoints use this method to scope results to the authenticated user.

src/db/models/task-model.ts
import { BasicDataModel, GetModel } from "@antelopejs/interface-database-decorators";
import { Task } from "../tables/task";

class TaskModel extends BasicDataModel(Task) {
  async getTasksByUserId(userId: string) {
    return this.table.filter({ userId }).run();
  }
}

export function getTaskModel() {
  return GetModel(TaskModel);
}

export { TaskModel };

Build the authentication routes

The auth controller handles user registration, login, and profile retrieval. Registration creates a new user, login validates credentials and returns a signed token, and the profile endpoint demonstrates the @Authentication decorator.

src/routes/auth.ts
import { Controller, Get, Post, JSONBody, HTTPResult } from "@antelopejs/interface-api";
import { SignRaw, ValidateRaw, Authentication } from "@antelopejs/interface-auth";
import { getUserModel } from "../db/models/user-model";

class AuthController extends Controller("/api/auth") {
  @Post("register")
  async register(@JSONBody() body: { name: string; email: string; password: string }) {
    const userModel = getUserModel();
    try {
      const user = await userModel.createUser(body);
      const token = await SignRaw(
        { userId: user._id, email: user.email },
        { expiresIn: "24h" }
      );
      return { token, user: { id: user._id, name: user.name, email: user.email } };
    } catch (error: any) {
      return new HTTPResult(400, { error: error.message });
    }
  }

  @Post("login")
  async login(@JSONBody() body: { email: string; password: string }) {
    const userModel = getUserModel();
    const user = await userModel.getUserByEmail(body.email);
    if (!user) {
      return new HTTPResult(401, { error: "Invalid credentials" });
    }

    const token = await SignRaw(
      { userId: user._id, email: user.email },
      { expiresIn: "24h" }
    );

    return { token };
  }

  @Get("profile")
  async profile(@Authentication() auth: any) {
    const userModel = getUserModel();
    const user = await userModel.get(auth.userId);
    if (!user) {
      return new HTTPResult(404, { error: "User not found" });
    }
    return { id: user._id, name: user.name, email: user.email };
  }
}

The register endpoint creates a user and immediately returns a token so the client can start making authenticated requests. The login endpoint validates the email and password, then issues a token. The profile endpoint uses @Authentication to ensure only authenticated users can access it.

The @Hashed modifier on the User table handles password hashing transparently. When the user model inserts a record, the password is hashed before it reaches the database. The hash modifier also handles password comparison during login.

Create the Task Data API

The Data API generates CRUD endpoints for the Task table with a single class definition. Each route is wrapped with DefaultRoutes.WithOptions to require authentication.

src/data-api/tasks.ts
import { DataController, RegisterDataController, DefaultRoutes } from "@antelopejs/interface-data-api";
import { Controller } from "@antelopejs/interface-api";
import { Authentication } from "@antelopejs/interface-auth";
import { Task } from "../db/tables/task";

@RegisterDataController()
class TaskDataController extends DataController(
  Task,
  {
    get: DefaultRoutes.WithOptions(DefaultRoutes.Get, { auth: Authentication }),
    list: DefaultRoutes.WithOptions(DefaultRoutes.List, { auth: Authentication }),
    new: DefaultRoutes.WithOptions(DefaultRoutes.New, { auth: Authentication }),
    edit: DefaultRoutes.WithOptions(DefaultRoutes.Edit, { auth: Authentication }),
    delete: DefaultRoutes.WithOptions(DefaultRoutes.Delete, { auth: Authentication }),
  },
  Controller("/api/tasks")
) {}

The Data API definition replaces dozens of lines of manual route handlers. The framework reads the Task table's schema and generates endpoints that accept and return data matching the table structure.

Write the entry point

The module entry point ties everything together. The construct function initializes the database schema and registers interface implementations. The start function begins listening for HTTP requests. Importing the route and data-api files ensures they are registered with the framework.

src/index.ts
import { CreateDatabaseSchemaInstance } from "@antelopejs/interface-database-decorators";

// Import tables to register them
import "./db/tables/user";
import "./db/tables/task";

// Import routes and data controllers to register them
import "./routes/auth";
import "./data-api/tasks";

export async function construct(): Promise<void> {
  // Create the database schema
  await CreateDatabaseSchemaInstance("main");
}

export function start(): void {
  // Application is ready
}

export async function stop(): Promise<void> {
  // Gracefully stop operations
}

export async function destroy(): Promise<void> {
  // Clean up resources
}

The import statements at the top ensure that all @RegisterTable, Controller, and @RegisterDataController() declarations execute and register their targets with the framework. The CreateDatabaseSchemaInstance("main") call initializes the database schema that both tables reference. This module consumes interfaces from other modules but does not export any of its own, so there is no ImplementInterface call.

Configure the project

The antelope.config.ts file defines the project structure and module configuration. It tells the AntelopeJS core where to find your module and any implementation modules for the interfaces you depend on.

antelope.config.ts
import { defineConfig } from "@antelopejs/interface-core/config";

export default defineConfig({
  name: "task-manager",
  modules: {
    "my-app": {
      source: { type: "local", path: "./" },
    },
  },
});

You also need implementation modules for the interfaces your application uses (API server, database driver, auth provider). Add them to the modules section based on the implementations available for your stack.

Run the application

Start the development server with the AntelopeJS CLI:

ajs project dev

The core loads the module, initializes the database schema, registers all controllers, and starts the HTTP server. For automatic reloading during development, add the --watch flag:

ajs project dev --watch

API Summary

The completed application exposes the following endpoints:

MethodEndpointAuthDescription
POST/api/auth/registerNoCreate a new user account
POST/api/auth/loginNoAuthenticate and receive a token
GET/api/auth/profileYesRetrieve the authenticated user's profile
GET/api/tasksYesList all tasks
GET/api/tasks/:idYesGet a single task
POST/api/tasksYesCreate a new task
PUT/api/tasks/:idYesUpdate an existing task
DELETE/api/tasks/:idYesDelete a task

Authenticated endpoints require a valid JWT token in the x-antelopejs-auth header:

x-antelopejs-auth: <token>