Getting Started

Project Structure

Arkos follows a file-based convention system. Certain files with specific naming patterns are automatically discovered and integrated by the framework at boot time. This page covers the required structure and explains what each file type does.

Files and folders marked as required must exist at the specified paths for Arkos to function correctly. Everything else follows community best practices but is flexible.

Root Directory

my-arkos-project/
├── prisma/
│   └── schema/
│       └── schema.prisma        # Database schema (required)
├── src/
│   ├── modules/                 # Feature modules (required)
│   ├── utils/
│   │   └── prisma/
│   │       └── index.ts         # Prisma client export (required)
│   └── app.ts                   # Application entry point (required)
├── uploads/                     # File storage directory
├── .env
├── package.json
└── arkos.config.ts              # Framework configuration (v1.4.0+)

Application Entry Point

src/app.ts
import arkos from "arkos";
import analyticsRouter from "@/src/modules/analytics/analytics.router";

const app = arkos();

app.use(analyticsRouter);

app.listen();
src/app.ts
import arkos from "arkos";
import analyticsRouter from "./routers/analytics.router";

arkos.init({
  use: [analyticsRouter],
  configureApp: (app) => {
    app.set("trust proxy", 1);
  },
});
src/app.ts
import arkos from "arkos";
import analyticsRouter from "./routers/analytics.router";

arkos.init({
  cors: { allowedOrigins: "*" },
  routers: {
    additional: [analyticsRouter],
  },
});

Prisma Client — Required

Arkos dynamically imports your Prisma client to power auto-generated endpoints and the BaseService class. It must be exported as default from exactly this path:

src/utils/prisma/index.ts
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export default prisma;

Modules Directory

Each Prisma model gets its own module directory under src/modules/. Here's the full set of files a module can have:

src/modules/post/
├── post.controller.ts       # Custom controller logic
├── post.service.ts          # Business logic and data operations
├── post.router.ts           # Route definitions and RouteHook config
├── post.interceptors.ts     # Before/after hooks for built-in routes
├── post.auth.ts             # Auth config files (pre-v1.6, still supported)
├── post.policy.ts           # ArkosPolicy (v1.6+, recommended)
├── post.query.ts            # Default Prisma query options
├── post.hooks.ts            # Service-layer hooks
├── dtos/                    # Class-validator DTOs
│   ├── create-post.dto.ts
│   └── update-post.dto.ts
└── schemas/                 # Zod schemas
    ├── create-post.schema.ts
    └── update-post.schema.ts

Module directory names must be kebab-case and match the Prisma model name (user-profile, not userProfile or UserProfile). Arkos uses these names to match modules to their models.

Controller (*.controller.ts)

For Prisma model routes, extend BaseController which already provides createOne, findMany, findOne, updateOne, updateMany, deleteOne, deleteMany, and createMany. Add custom methods on top:

src/modules/post/post.controller.ts
import { BaseController } from "arkos";
import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos";
import postService from "./post.service";

class PostController extends BaseController {
  getAnalytics = async (
    req: ArkosRequest,
    res: ArkosResponse,
    next: ArkosNextFunction
  ) => {
    const analytics = await postService.getAnalytics();
    res.json({ status: "success", data: analytics });
  };
}

export default new PostController(postService);

Service (*.service.ts)

Extend BaseService to add business logic on top of the built-in CRUD methods:

src/modules/post/post.service.ts
import { BaseService } from "arkos/services";

class PostService extends BaseService<"post"> {
  async getPublished() {
    return this.findMany({ where: { published: true } });
  }

  async getAnalytics() {
    return this.findMany({
      where: { published: true },
      select: { id: true, title: true, views: true },
    });
  }
}

export default new PostService("post");
src/modules/post/post.service.ts
import { BaseService } from "arkos/services";
import { Prisma } from "@prisma/client";

class PostService extends BaseService<Prisma.PostDelegate> {
  async getPublished() {
    return this.findMany({ where: { published: true } });
  }
}

export default new PostService("post");

Router (*.router.ts)

Defines custom routes and configures built-in auto-generated routes via RouteHook:

src/modules/post/post.router.ts
import { ArkosRouter, RouteHook } from "arkos";
import postPolicy from "./post.policy";
import postController from "./post.controller";

export const hook: RouteHook = {
  findMany: { authentication: false },
  createOne: { authentication: postPolicy.Create },
  deleteOne: { authentication: postPolicy.Delete },
};

const postRouter = ArkosRouter();

postRouter.get(
  { path: "/posts/analytics", authentication: postPolicy.View },
  postController.getAnalytics
);

export default postRouter;

RouteHook is the new name for export const config: RouterConfig. If you have existing code using the old name it still works but will log a deprecation warning. See Route Hook for full details.

Interceptors (*.interceptors.ts)

Run logic before or after auto-generated route operations without replacing the built-in behavior. For custom routes, this logic lives in the controller instead:

src/modules/post/post.interceptors.ts
import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos";

export const beforeCreateOne = [
  async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => {
    req.body.authorId = req.user.id;
    next();
  },
];

export const afterCreateOne = [
  async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => {
    const created = res.locals.data.data;
    console.log("Post created:", created.id);
    next();
  },
];

export const onCreateOneError = [
  async (
    err: any,
    req: ArkosRequest,
    res: ArkosResponse,
    next: ArkosNextFunction
  ) => {
    next(err);
  },
];

See Interceptors for the full list of available hooks.

Policy (*.policy.ts)

Defines permissions for a module using ArkosPolicy (v1.6+, recommended):

src/modules/post/post.policy.ts
import { ArkosPolicy } from "arkos";

const postPolicy = ArkosPolicy("post")
  .rule("Create", { roles: ["Admin", "Editor"], name: "Create Post" })
  .rule("Update", { roles: ["Admin", "Editor"], name: "Update Post" })
  .rule("Delete", { roles: ["Admin"], name: "Delete Post" })
  .rule("View", { roles: ["*"] });

export default postPolicy;

See Static Mode for full details. For projects using the older .auth.ts approach, see Auth Config Files.

Service Hooks (*.hooks.ts)

Run logic at the service layer — fires on every BaseService call, whether from an HTTP endpoint or programmatically:

src/modules/post/post.hooks.ts
import { BeforeCreateOneHookArgs, AfterCreateOneHookArgs } from "arkos/services";
import { Prisma } from "@prisma/client";

export const beforeCreateOne = [
  async ({ data }: BeforeCreateOneHookArgs<Prisma.PostDelegate>) => {
    if (!data.slug && data.title) {
      data.slug = data.title.toLowerCase().replace(/\s+/g, "-");
    }
  },
];

export const afterCreateOne = [
  async ({ result }: AfterCreateOneHookArgs<Prisma.PostDelegate>) => {
    console.log("Post created via service:", result.id);
  },
];

See Service Hooks for the full list of available hooks and how they differ from interceptors.

Prisma Query Options (*.query.ts)

Define default query parameters that Arkos applies automatically to auto-generated endpoints:

src/modules/post/post.query.ts
import { Prisma } from "@prisma/client";
import { PrismaQueryOptions } from "arkos/prisma";

const postPrismaQueryOptions: PrismaQueryOptions<Prisma.PostDelegate> = {
  findMany: {
    include: {
      author: {
        select: { id: true, name: true },
      },
      tags: true,
    },
    orderBy: { createdAt: "desc" },
  },
  findOne: {
    include: {
      author: true,
      tags: true,
      comments: {
        orderBy: { createdAt: "desc" },
      },
    },
  },
};

export default postPrismaQueryOptions;

Validation

Zod schemas (schemas/*.schema.ts):

src/modules/post/schemas/create-post.schema.ts
import z from "zod";

const CreatePostSchema = z.object({
  title: z.string().max(200),
  content: z.string().min(10),
  excerpt: z.string().optional(),
});

export default CreatePostSchema;

Class-validator DTOs (dtos/*.dto.ts):

src/modules/post/dtos/create-post.dto.ts
import { IsString, IsOptional, MaxLength } from "class-validator";

export default class CreatePostDto {
  @IsString()
  @MaxLength(200)
  title: string;

  @IsString()
  content: string;

  @IsOptional()
  @IsString()
  excerpt?: string;
}

Package.json Scripts

"type": "module" is only required for JavaScript projects. Remove it if you are using TypeScript.

package.json
{
  "type": "module",
  "scripts": {
    "dev": "arkos dev",
    "build": "arkos build",
    "start": "arkos start",
    "arkos": "arkos"
  },
  "prisma": {
    "schema": "prisma/schema/"
  }
}

Naming Conventions

ConventionRule
Module directorieskebab-case matching Prisma model name — user-profile, order-item
File namingmodel-name.file-type.tspost.controller.ts, post.service.ts
Controller exportAlways a class singleton — export default new PostController(postService)
Service exportAlways a class singleton — export default new PostService("post")
InterceptorsAlways arrays — export const beforeCreateOne = [...]
Service hooksAlways arrays — export const beforeCreateOne = [...]