# Fine-Grained Access Control Route-level permissions answer "can this user access this endpoint?" — that covers most applications. Fine-grained access control goes further: "can this user perform this action on this resource under this condition?" It's what lets you express rules like: only managers can modify a completed order, or a user can only edit their own posts. Most apps are well served by route-level permissions alone. Before reaching for FGAC, make sure you have authentication and permissions configured — see [Authentication Setup](/docs/core-concepts/authentication/setup), [Static Mode](/docs/core-concepts/authentication/permissions/static), and [Dynamic Mode](/docs/core-concepts/authentication/permissions/dynamic). ## The Problem [#the-problem] Consider `PATCH /api/orders/:id`. Regular staff can update pending orders — straightforward route-level access. But what if an order is already completed and someone made a mistake? Not all staff should be able to touch it. Only managers should. That's a condition route-level auth can't express — that's where FGAC comes in. ## Defining Permissions [#defining-permissions] **v1.6+**: Use `ArkosPolicy` — the recommended API. It replaces the previous `authService.permission()` pattern from `.auth.ts` files. Both are supported, but `ArkosPolicy` is the current design, but your `.auth.ts` files still work, nothing breaks — see [Auth Config Files](#auth-config-files-before-v16) below. Define granular named permissions for your module using `ArkosPolicy`: ```ts title="src/modules/order/order.policy.ts" import { ArkosPolicy } from "arkos"; const orderPolicy = ArkosPolicy("order") .rule("Update", { roles: ["OrderStaff", "OrderManager", "Admin"], name: "Update Orders" }) .rule("UpdateCompleted", { roles: ["OrderManager", "Admin"], name: "Update Completed Orders", description: "Modify orders already marked as completed — restricted" }) .rule("Complete", { roles: ["OrderManager", "Admin"], name: "Complete Orders" }) .rule("Delete", { roles: ["Admin"], name: "Delete Orders" }); export default orderPolicy; ``` Policy definitions must live at module level — never inside request handlers. Arkos discovers all permissions at startup to expose them through `/api/auth-actions`. This is how frontend developers know what actions exist in your system. Generate a policy file with the CLI: ```bash arkos generate policy --module order # or arkos g p -m order ``` ### Static vs Dynamic Mode [#static-vs-dynamic-mode] `ArkosPolicy` works identically in both [Static](/docs/core-concepts/authentication/permissions/static) and [Dynamic](/docs/core-concepts/authentication/permissions/dynamic) mode. The only difference is that `roles` inside rules are enforced in Static mode and ignored in Dynamic mode — enforcement comes from the database instead. In Dynamic mode, define your rules without roles: ```ts title="src/modules/order/order.policy.ts" import { ArkosPolicy } from "arkos"; const orderPolicy = ArkosPolicy("order") .rule("Update", { name: "Update Orders" }) .rule("UpdateCompleted", { name: "Update Completed Orders", description: "Modify orders already marked as completed — restricted" }) .rule("Complete", { name: "Complete Orders" }) .rule("Delete", { name: "Delete Orders" }); export default orderPolicy; ``` ## Fine Grained Access Control In Custom Routes [#fine-grained-access-control-in-custom-routes] For custom `ArkosRouter` routes, call `can*` methods directly inside your handler or middleware: ```ts title="src/modules/order/order.router.ts" import { ArkosRouter } from "arkos"; import orderPolicy from "@/src/modules/order/order.policy"; import orderController from "@/src/modules/order/order.controller"; import orderService from "@/src/modules/order/order.service"; import { AppError } from "arkos/error-handler"; const router = ArkosRouter(); router.patch( { path: "/api/orders/:id", authentication: orderPolicy.Update, }, async (req, res, next) => { const order = await orderService.findOne({ id: req.params.id }); if (!order) throw new AppError("Order not found", 404); if (order.status === "Completed") { const canUpdateCompleted = await orderPolicy.canUpdateCompleted(req.user); if (!canUpdateCompleted) throw new AppError( "You don't have permission to modify completed orders. Contact your manager.", 403, "CannotUpdateCompletedOrder" ); } next(); }, orderController.updateOne ); export default router; ``` ## Fine Grained Access Control In Interceptors For Built-in Routes [#fine-grained-access-control-in-interceptors-for-built-in-routes] For built-in routes the condition logic of fine grained access control lives in interceptor middlewares. For auto-generated routes this is the primary pattern — fetch the record, check the condition, branch on permissions: ```ts title="src/modules/order/order.interceptors.ts" import { AppError } from "arkos/error-handler"; import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; import orderPolicy from "@/src/modules/order/order.policy"; import orderService from "@/src/modules/order/order.service"; export const beforeUpdateOne = [ async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const order = await orderService.findOne({ id: req.params.id }); if (!order) throw new AppError("Order not found", 404); req.locals = { order }; next(); }, async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const { order } = req.locals; const user = req.user; if (order.status === "Completed") { const canUpdateCompleted = await orderPolicy.canUpdateCompleted(user); if (!canUpdateCompleted) throw new AppError( "You don't have permission to modify completed orders. Contact your manager.", 403, "CannotUpdateCompletedOrder" ); } if (req.body.status === "Completed" && order.status !== "Completed") { const canComplete = await orderPolicy.canComplete(user); if (!canComplete) throw new AppError( "You don't have permission to mark orders as completed.", 403 ); req.body.completedAt = new Date(); } next(); }, ]; ``` With this in place: * **Cacilda** (OrderStaff) tries to update a completed order → `403 CannotUpdateCompletedOrder` * **Sheuzia** (OrderManager) updates the same order → passes, order is modified ## Auth Config Files (before v1.6) [#auth-config-files-before-v16] Before `ArkosPolicy`, fine-grained permissions were defined via `authService.permission()` inside `.auth.ts` files. This is still fully supported — nothing breaks. New projects should use `ArkosPolicy`. ```ts title="src/modules/order/order.auth.ts" import { AuthConfigs } from "arkos/auth"; import { authService } from "arkos/services"; const orderAccessControl = { Update: { roles: ["OrderStaff", "OrderManager", "Admin"], name: "Update Orders" }, UpdateCompleted: { roles: ["OrderManager", "Admin"], name: "Update Completed Orders" }, Complete: { roles: ["OrderManager", "Admin"], name: "Complete Orders" }, Delete: { roles: ["Admin"], name: "Delete Orders" }, } export const orderPermissions = { canUpdate: authService.permission("Update", "order", orderAccessControl), canUpdateCompleted: authService.permission("UpdateCompleted", "order", orderAccessControl), canComplete: authService.permission("Complete", "order", orderAccessControl), canDelete: authService.permission("Delete", "order", orderAccessControl), }; const orderAuthConfigs: AuthConfigs = { authenticationControl: { Update: true, UpdateCompleted: true, Complete: true, Delete: true, }, accessControl: orderAccessControl, }; export default orderAuthConfigs; ``` Then use `orderPermissions.canUpdateCompleted(user)` in your middlewares, services and interceptors exactly as shown [above examples](#fine-grained-access-control-in-custom-routes) with `orderPolicy.canUpdateCompleted(user)` — the usage is identical. Generate an auth config file: ```bash arkos generate auth-configs --module order # or arkos g a -m order ``` # Folder-Level Access Control This will help you define files access level through folders, still under discussion if it is going to be useful leave your thoughts at [Folder-Level Access Control Issue #159](https://github.com/uanela/arkos/issues/159). # Hooks > Available since v1.6.0-beta Arkos runs a two-step pipeline on every protected route: first it authenticates the request via `authService.authenticate` (extracts and verifies the JWT, sets `req.user`), then it authorizes it via `authService.authorize` (checks the user's role or permissions against the route's access control rules). These are the same two methods that `ArkosRouter` and `RouteHook` wire under the hood when you pass an `authentication` option to a route. Authentication hooks let you tap into both steps — running logic before the core check, after it passes, or when it fails — without replacing the built-in behavior. Hooks are defined once in `ArkosConfig` and apply globally to every route that goes through the pipeline, whether that's an auto-generated Prisma model route, a built-in auth endpoint, or a custom `ArkosRouter` route. `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](/docs/core-concepts/components/route-hooks) for full details. ## Configuration [#configuration] ```ts title="arkos.config.ts" import { defineConfig } from "arkos"; import { AppError } from "arkos/error-handler"; export default defineConfig({ authentication: { mode: "static", hooks: { authenticate: { before: ({ req, skip }) => { /* runs before JWT verification */ }, after: ({ req }) => { /* runs after req.user is set */ }, onError: ({ req, error, skip }) => { /* runs when verification fails */ }, }, authorize: { before: ({ req, action, resource, rule, skip }) => { /* runs before permission check */ }, after: ({ req, action, resource, rule }) => { /* runs after check passes */ }, onError: ({ req, error, action, resource, rule, skip }) => { /* runs when check fails (403) */ }, }, }, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { authentication: { mode: "static", hooks: { authenticate: { before: ({ req, skip }) => { /* runs before JWT verification */ }, after: ({ req }) => { /* runs after req.user is set */ }, onError: ({ req, error, skip }) => { /* runs when verification fails */ }, }, authorize: { before: ({ req, action, resource, rule, skip }) => { /* runs before permission check */ }, after: ({ req, action, resource, rule }) => { /* runs after check passes */ }, onError: ({ req, error, action, resource, rule, skip }) => { /* runs when check fails (403) */ }, }, }, }, }; export default arkosConfig; ``` ```ts title="src/app.ts" import arkos from "arkos"; arkos.init({ authentication: { mode: "static", hooks: { authenticate: { before: ({ req, skip }) => { /* runs before JWT verification */ }, after: ({ req }) => { /* runs after req.user is set */ }, onError: ({ req, error, skip }) => { /* runs when verification fails */ }, }, authorize: { before: ({ req, action, resource, rule, skip }) => { /* runs before permission check */ }, after: ({ req, action, resource, rule }) => { /* runs after check passes */ }, onError: ({ req, error, action, resource, rule, skip }) => { /* runs when check fails (403) */ }, }, }, }, }); ``` Each hook can be a single function or an array of functions. When an array is provided, they run in order — the chain stops if one throws or calls `skip()`. ## authenticate Hooks [#authenticate-hooks] These hooks wrap `authService.authenticate` — the JWT extraction and verification step. When this step runs, Arkos reads the token from the `Authorization` header or the `arkos_access_token` cookie, verifies it, fetches the user from the database, and sets `req.user`. ### Execution Flow [#execution-flow] # Dynamic Dynamic mode is Arkos's database-driven permission system. Roles and permissions live in `AuthRole`, `AuthPermission`, and `UserRole` models and can be created, updated, and assigned at runtime without a redeploy — making it the right choice for multi-tenant apps, SaaS platforms, or any system where roles change frequently. Before using Dynamic mode make sure you have authentication configured. See [Authentication Setup](/docs/core-concepts/authentication/setup). ## User Model [#user-model] Dynamic mode replaces the `role`/`roles` enum field with a `UserRole` relation, and adds three required models: ```prisma title="prisma/schema.prisma" model User { // ... required Arkos fields roles UserRole[] // your own fields email String? @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model AuthRole { id String @id @default(uuid()) name String @unique permissions AuthPermission[] users UserRole[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model AuthPermission { id String @id @default(uuid()) resource String action String roles AuthRole[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([resource, action]) } model UserRole { id String @id @default(uuid()) userId String roleId String user User @relation(fields: [userId], references: [id]) role AuthRole @relation(fields: [roleId], references: [id]) @@unique([userId, roleId]) } ``` Before 1.7.0, `AuthPermission` had a `roleId` field and `@@unique([resource, action, roleId])` — one row per role. 1.7.0 made permissions shared across roles via a many-to-many `roles` relation and changed the unique constraint to `@@unique([resource, action])`. See [Migrating from Static](#migrating-from-static) if you're upgrading an existing Dynamic-mode project. See the full required User model at [User Model Authentication Setup](/docs/core-concepts/authentication/setup#user-model). ## Configuration [#configuration] Change `mode` to `"dynamic"` — everything else stays the same as the base [Authentication Setup](/docs/core-concepts/authentication/setup#configuration): ```ts title="arkos.config.ts" import { defineConfig } from "arkos"; export default defineConfig({ authentication: { mode: "dynamic", // ... rest of your config unchanged }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { authentication: { mode: "dynamic", // ... rest of your config unchanged }, }; export default arkosConfig; ``` ```ts title="src/app.ts" import arkos from "arkos"; arkos.init({ authentication: { mode: "dynamic", // ... rest of your config unchanged }, }); ``` ## Defining Dynamic Permissions [#defining-dynamic-permissions] Same `ArkosPolicy` and `.auth.ts` API as [Static mode](/docs/core-concepts/authentication/permissions/static) — the only difference is that `roles` inside rules are ignored at enforcement time. Actual enforcement comes from database records instead. Define your policy with names and descriptions for discovery, and skip the roles — or keep `roles: ["*"]` where all authenticated users should have access: ```ts title="src/modules/post/post.policy.ts" import { ArkosPolicy } from "arkos"; const postPolicy = ArkosPolicy("post") .rule("Create", { name: "Create Post", description: "Create new posts" }) .rule("Update", { name: "Update Post" }) .rule("Delete", { name: "Delete Post" }) .rule("View", { roles: ["*"] }); // * still works — all authenticated users export default postPolicy; ``` Wiring to routes is identical to Static mode — see [Static Mode — Using Permissions in Routes](/docs/core-concepts/authentication/permissions/static#using-permissions-in-routes). ## Managing Permissions [#managing-permissions] Arkos auto-generates full CRUD endpoints for `AuthRole`, `AuthPermission`, and `UserRole`: ``` GET /api/auth-roles POST /api/auth-roles PATCH /api/auth-roles/:id DELETE /api/auth-roles/:id GET /api/auth-permissions POST /api/auth-permissions PATCH /api/auth-permissions/:id DELETE /api/auth-permissions/:id GET /api/user-roles POST /api/user-roles DELETE /api/user-roles/:id ``` Or manage them programmatically: ```ts const adminRole = await prisma.authRole.create({ data: { name: "Admin" } }); const editorRole = await prisma.authRole.create({ data: { name: "Editor" } }); const createPost = await prisma.authPermission.create({ data: { resource: "post", action: "Create", roles: { connect: { id: editorRole.id } }, }, }); const deletePost = await prisma.authPermission.create({ data: { resource: "post", action: "Delete", roles: { connect: { id: adminRole.id } }, }, }); await prisma.userRole.create({ data: { userId: user.id, roleId: adminRole.id }, }); ``` ## User-Level Permission Overrides [#user-level-permission-overrides] > Available from 1.7.0-rc Sometimes you need to grant or revoke a specific permission for one user without creating a one-off role for them. `UserPermission` sits on top of role-derived permissions and lets you override the result per user, per permission: ```prisma title="prisma/schema.prisma" model UserPermission { id String @id @default(uuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt effect UserPermissionEffect @default(Allow) userId String user User @relation(fields: [userId], references: [id]) permissionId String permission AuthPermission @relation(fields: [permissionId], references: [id]) @@unique([userId, permissionId]) } enum UserPermissionEffect { Allow Deny } // Update the user to add new relation model User { // ... everything else remains the same permissions UserPermission[] } // Update it to add the new relation model AuthPermission { // everything else remains the same users UserPermission[] } ``` **Resolution order** — this is exactly how `checkDynamicAccessControl` decides access: 1. If a `UserPermission` row exists for `(userId, permissionId)`, its `effect` wins — full stop, regardless of role. 2. Otherwise, falls back to whatever the user's `AuthRole` grants. If your Prisma client has no `userPermissions` delegate — i.e. you haven't added the model yet — Arkos skips the override lookup entirely and falls back to role-derived permission. No error, no behavior change. Safe to leave out until you actually need per-user overrides. Already on Dynamic mode and want user-level overrides on an existing project? See [Adding User Permissions in Old Projects](/blogs/adding-user-permissions-in-old-projects) for the schema changes and migration steps. ## Imperative Checks [#imperative-checks] `ArkosPolicy` `can*` methods work in Dynamic mode too, checking against database permissions. See [Fine-Grained Access Control](/docs/core-concepts/authentication/advanced/fine-grained-access-control) for full usage. Imperative checks in fine-grained access control also work with auth config files. See the full guide at [Fine-Grained Access Control](/docs/core-concepts/authentication/advanced/fine-grained-access-control). ## Migrating from Static [#migrating-from-static] 1. Add `AuthRole`, `AuthPermission`, `UserRole` models to your schema 2. Replace `role`/`roles` enum field on `User` with `roles UserRole[]` 3. Run `arkos prisma generate` 4. Change `mode` to `"dynamic"` in your config 5. Create `AuthRole` records matching your previous enum values 6. Create `AuthPermission` records based on your existing rules 7. Assign users to roles via `UserRole` 8. Remove `roles` from your policy rules — they'll be ignored anyway 9. **Optional:** for per-user overrides, add the `UserPermission` model and `UserPermissionEffect` enum to your schema, add `permissions UserPermission[]` to `User`, then run `arkos prisma generate` # Static Static mode is Arkos's code-based permission system. Roles are assigned on the `User` model as an enum field, and permissions are declared via [`ArkosPolicy`](#arkospolicy) (v1.6+, recommended) or [`.auth.ts` files](#auth-files) (v1.1+, still supported). Arkos enforces them automatically across all routes. Before using Static mode make sure you have authentication configured. See [Authentication Setup](/docs/core-concepts/authentication/setup). ## User Model [#user-model] Static mode requires a `role` or `roles` enum field on your `User` model: ```prisma title="prisma/schema.prisma" enum UserRole { Admin Editor User } model User { // ... required Arkos fields role UserRole @default(User) // single role // roles UserRole[] // multiple roles } ``` See the full required User model at [User Model Authentication Setup](/docs/core-concepts/authentication/setup#user-model). ## ArkosPolicy [#arkospolicy] `ArkosPolicy` is the recommended API introduced in v1.6. It provides a fluent interface for defining permissions, works for any module (Prisma models, auth, file upload, or custom), and supports imperative `can*` checks for [Fine-Grained Access Control](/docs/core-concepts/authentication/advanced/fine-grained-access-control). ### Defining a Policy [#defining-a-policy] ```ts title="src/modules/post/post.policy.ts" import { ArkosPolicy } from "arkos"; const postPolicy = ArkosPolicy("post") .rule("Create", { roles: ["Admin", "Editor"], name: "Create Post", description: "Create new posts" }) .rule("Update", { roles: ["Admin", "Editor"], name: "Update Post" }) .rule("Delete", { roles: ["Admin"], name: "Delete Post" }) .rule("View", { roles: ["*"] }); // all authenticated users export default postPolicy; ``` `.rule(action, rule)` accepts: | Value | Behavior | | --------------------------------------- | ------------------------- | | `{ roles: ["*"] }` | All authenticated users | | `{ roles: [...], name?, description? }` | Role-restricted | | `["Admin", "Editor"]` | Shorthand for roles array | ### Using Permissions in Routes [#using-permissions-in-routes] Permissions wire the same way whether you're on a custom route or a built-in one. For built-in routes (Prisma model, auth, file upload) use a [Route Hook](/docs/core-concepts/components/route-hooks) alongside your `ArkosRouter` export. ```ts title="src/modules/post/post.router.ts" import { ArkosRouter } from "arkos"; import postPolicy from "@/src/modules/post/post.policy"; import postController from "@/src/modules/post/post.controller"; const router = ArkosRouter(); router.post( { path: "/api/posts", authentication: postPolicy.Create }, postController.createOne ); router.get( { path: "/api/posts", authentication: postPolicy.View }, postController.findMany ); export default router; ``` ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import postPolicy from "@/src/modules/post/post.policy"; export const hook: RouteHook = { createOne: { authentication: postPolicy.Create }, findMany: { authentication: postPolicy.View }, updateOne: { authentication: postPolicy.Update }, deleteOne: { authentication: postPolicy.Delete }, }; const router = ArkosRouter(); export default router; ``` `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](/docs/core-concepts/components/route-hooks) for full details. ### Using Permissions in Express Router [#using-permissions-in-express-router] If you're using a plain Express `Router` instead of `ArkosRouter`, wire authentication manually via `authService`: ```ts title="src/modules/post/post.router.ts" import { Router } from "express"; import { authService } from "arkos/services"; import postPolicy from "@/src/modules/post/post.policy"; import postController from "@/src/modules/post/post.controller"; const router = Router(); // login required only router.get("/api/posts", authService.authenticate, postController.findMany); // role-based — v1.6+ router.post( "/api/posts", authService.authenticate, authService.authorize("Create", "post", postPolicy.Create), postController.createOne ); ``` | Method | Signature | Version | | --------------------------------- | ----------------------------------- | ----------- | | `authService.authenticate` | middleware | all | | `authService.authorize` | `(action, resource, rule)` | v1.6+ | | `authService.handleAccessControl` | `(action, resource, accessControl)` | before v1.6 | ### Imperative Checks [#imperative-checks] `ArkosPolicy` exposes `can*` methods for use inside services, interceptors, or any custom logic — this is the foundation of [Fine-Grained Access Control](/docs/core-concepts/authentication/advanced/fine-grained-access-control): ```ts title="src/modules/post/post.service.ts" import postPolicy from "@/src/modules/post/post.policy"; if (await postPolicy.canDelete(req.user)) { // proceed } ``` Imperative checks in fine-grained access control also work with auth config files. See the full guide at [Fine-Grained Access Control](/docs/core-concepts/authentication/advanced/fine-grained-access-control). ### CLI Generation (v1.6+) [#cli-generation-v16] ```bash arkos generate policy --module post # or arkos g p -m post ``` ## Auth Config Files [#auth-config-files] `.auth.ts` auto-loading is deprecated as of v1.6 and will be removed in v2.0. New projects should use `ArkosPolicy`. Existing projects can migrate gradually — see the [migration guide](/blog/how-migrate-from-auth-files-to-arkos-policy). `.auth.ts` files have been the standard since v1.0 and remain fully supported. They define `authenticationControl` (who needs to be logged in) and `accessControl` (which roles can act) separately. ### Creating an Auth File [#creating-an-auth-file] ```bash arkos generate auth-configs --module post # or arkos g a -m post ``` This generates `src/modules/post/post.auth.ts`: ```ts title="src/modules/post/post.auth.ts" import { AuthConfigs } from "arkos/auth"; export const postAccessControl = { Create: { roles: ["Admin", "Editor"], name: "Create Post", description: "Permission to create new post records", }, Update: { roles: ["Admin", "Editor"], name: "Update Post", description: "Permission to update existing post records", }, Delete: { roles: ["Admin"], name: "Delete Post", description: "Permission to delete post records", }, View: { roles: ["*"], name: "View Post", description: "Permission to view post records", }, } as const satisfies AuthConfigs["accessControl"]; export const postAuthenticationControl = { Create: true, Update: true, Delete: true, View: true, }; const postAuthConfigs: AuthConfigs = { authenticationControl: postAuthenticationControl, accessControl: postAccessControl, }; export default postAuthConfigs; ``` ### Auth Config File Structure [#auth-config-file-structure] **`authenticationControl`** — whether a route requires login: | Value | Behavior | | ------- | -------------------------- | | `true` | Login required | | `false` | Public — no authentication | **`accessControl`** — which roles can act: | Format | Example | | ----------------- | --------------------------------------------------- | | Simple | `["Admin", "Editor"]` | | Detailed | `{ roles: [...], name: "...", description: "..." }` | | All authenticated | `["*"]` | ### Using Auth Config Files in Routes [#using-auth-config-files-in-routes] The same auth config file works on both custom and built-in routes. For built-in routes use a [Route Hook](/docs/core-concepts/components/route-hooks) alongside your `ArkosRouter` export. ```ts title="src/modules/post/post.router.ts" import { ArkosRouter } from "arkos"; import postAuthConfigs from "@/src/modules/post/post.auth"; import postController from "@/src/modules/post/post.controller"; const router = ArkosRouter(); // login required only router.get( { path: "/api/posts", authentication: true, }, postController.findMany ); // role-based router.post( { path: "/api/posts", authentication: { resource: "post", action: "Create", rule: postAuthConfigs.accessControl.Create, }, }, postController.createOne ); export default router; ``` ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import postAuthConfigs from "@/src/modules/post/post.auth"; export const hook: RouteHook = { createOne: { authentication: { resource: "post", action: "Create", rule: postAuthConfigs.accessControl.Create, }, }, deleteOne: { authentication: { resource: "post", action: "Delete", rule: postAuthConfigs.accessControl.Delete, }, }, }; const router = ArkosRouter(); export default router; ``` `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](/docs/core-concepts/components/route-hooks) for full details. ### Using Auth Config Files in Express Router [#using-auth-config-files-in-express-router] ```ts title="src/modules/post/post.router.ts" import { Router } from "express"; import { authService } from "arkos/services"; import postAuthConfigs from "@/src/modules/post/post.auth"; import postController from "@/src/modules/post/post.controller"; const router = Router(); // login required only router.get("/api/posts", authService.authenticate, postController.findMany); // role-based — v1.6+ router.post( "/api/posts", authService.authenticate, authService.authorize("Create", "post", postAuthConfigs.accessControl.Create), postController.createOne ); // role-based — before v1.6 router.post( "/api/posts", authService.authenticate, authService.handleAccessControl("Create", "post", postAuthConfigs.accessControl), postController.createOne ); ``` ## Auth Actions Discovery [#auth-actions-discovery] Both `ArkosPolicy` and `.auth.ts` feed the `/api/auth-actions` endpoint — used by frontends to discover available permissions: ```json [ { "resource": "post", "action": "Create", "roles": ["Admin", "Editor"], "name": "Create Post", "description": "Permission to create new post records" } ] ``` Export them to a file for frontend integration: ```bash arkos export auth-action arkos export auth-action --overwrite arkos export auth-action --path src/constants ``` See [CLI reference](/docs/tooling/cli#export-auth-actions) for full options. # Routes Arkos provides a built-in authentication system with JWT and Role-Based Access Control (RBAC). Once configured, it automatically exposes the following endpoints — no route definitions needed. | Method | Endpoint | Description | Operation | | ------ | --------------------------- | ------------------- | ---------------- | | POST | `/api/auth/login` | Authenticate a user | `login` | | POST | `/api/auth/signup` | Register a new user | `signup` | | DELETE | `/api/auth/logout` | End session | `logout` | | POST | `/api/auth/update-password` | Change password | `updatePassword` | | GET | `/api/users/me` | Get current user | `getMe` | | PATCH | `/api/users/me` | Update current user | `updateMe` | | DELETE | `/api/users/me` | Delete current user | `deleteMe` | Authentication requires setup before these endpoints are active. See [Authentication Setup](/docs/core-concepts/authentication/setup) for configuration, User Model requirements, and RBAC options. ## Configuring Authentication Routes [#configuring-authentication-routes] Every auth endpoint accepts the same configuration object used in `ArkosRouter` — disable routes, add rate limiting, and more via the `config` export in `src/modules/auth/auth.router.ts` where each key maps to the operation name from the table above: ```ts title="src/modules/auth/auth.router.ts" import { ArkosRouter, RouterConfig } from "arkos"; import UpdateMeSchema from "@/src/modules/auth/schemas/update-me.schema" export const config: RouterConfig = { login: { rateLimit: { windowMs: 15 * 60_000, max: 10 } }, signup: { disabled: true }, getMe: { rateLimit: { windowMs: 15 * 60_000, max: 30 } }, deleteMe: { disabled: true }, udpateMe: { validation: { body: UpdateMeSchema } }, udpatePassword: { rateLimit: { windowMs: 15 * 60_000, max: 10 } }, }; const router = ArkosRouter(); export default router; ``` See the full configuration object reference at [ArkosRouter Configuration Object](/docs/reference/arkos-router#configuration-object). ## Intercepting Authentication Requests [#intercepting-authentication-requests] Every authentication endpoint can be intercepted — run logic before or after any operation without replacing built-in behavior. For example, to send a verification email after signup: ```typescript title="@/src/modules/auth/auth.interceptors.ts" import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; export const afterSignup = [ async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const user = res.locals.data.data; emailService.sendVerificationEmail(user.email).catch(console.error); next(); }, ]; ``` See [Interceptors](/docs/core-concepts/components/interceptors) for the full list of available auth hooks. ## Sending Requests [#sending-requests] ### Signup [#signup] ```http POST /api/auth/signup Content-Type: application/json ``` ```json { "username": "john_doe", "password": "SecurePassword123!", "email": "john@example.com", "firstName": "John", "lastName": "Doe" } ``` Response: ```json { "data": { "id": "abc123", "username": "john_doe", "email": "john@example.com", "firstName": "John", "lastName": "Doe", "isActive": true, "createdAt": "2025-04-05T14:23:45.123Z" } } ``` ### Login [#login] ```http POST /api/auth/login Content-Type: application/json ``` ```json { "username": "john_doe", "password": "SecurePassword123!" } ``` Response: ```json { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "data": { "id": "abc123", "username": "john_doe", "email": "john@example.com" } } ``` Depending on your `sendAccessTokenThrough` configuration, the token is set as a cookie, included in the response body, or both (default). See [Authentication Setup](/docs/core-concepts/authentication/setup) for configuration. #### Logging in with different fields [#logging-in-with-different-fields] By default users log in with `username`. Arkos supports logging in with other fields like `email`, or even nested relation fields like `profile.nickname`. ```http POST /api/auth/login Content-Type: application/json ``` ```json { "email": "john@example.com", "password": "SecurePassword123!" } ``` When using a non-default field, pass `usernameField` as a query parameter: ```http POST /api/auth/login?usernameField=email ``` See [Authentication Setup](/docs/core-concepts/authentication/setup#login-with-different-fields) for the full `allowedUsernames` configuration and nested field support. ### Logout [#logout] ```http DELETE /api/auth/logout ``` Clears the authentication cookie and invalidates the session. Response: `204 No Content`. ### Update Password [#update-password] ```http POST /api/auth/update-password Content-Type: application/json Authorization: Bearer YOUR_API_TOKEN ``` ```json { "currentPassword": "OldPassword123!", "newPassword": "NewSecurePassword123!" } ``` Response: ```json { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "data": { "id": "abc123", "username": "john_doe" } } ``` Since v1.5.0, Arkos automatically re-authenticates the user after a password update and returns a fresh token — no need to log in again. ### Get Current User [#get-current-user] ```http GET /api/users/me Authorization: Bearer YOUR_API_TOKEN ``` Response: ```json { "data": { "id": "abc123", "username": "john_doe", "email": "john@example.com", "firstName": "John", "lastName": "Doe", "isActive": true, "createdAt": "2025-04-05T14:23:45.123Z" } } ``` ### Update Current User [#update-current-user] ```http PATCH /api/users/me Content-Type: application/json Authorization: Bearer YOUR_API_TOKEN ``` ```json { "firstName": "John Updated", "email": "newemail@example.com" } ``` Response: ```json { "data": { "id": "abc123", "username": "john_doe", "email": "newemail@example.com", "firstName": "John Updated", "updatedAt": "2025-04-05T15:30:12.456Z" } } ``` ### Delete Current User [#delete-current-user] ```http DELETE /api/users/me Authorization: Bearer YOUR_API_TOKEN ``` Response: `204 No Content` ## Sending the Token [#sending-the-token] Authenticated endpoints accept the token either via the `Authorization` header or automatically via cookies (set on login). **Authorization header:** ```http GET /api/users/me Authorization: Bearer YOUR_API_TOKEN ``` **Cookie-based (set automatically on login):** ```javascript fetch("/api/users/me", { credentials: "include", }); ``` ## Error Responses [#error-responses] ```json // 401 - Invalid or missing token { "code": "Unauthorized", "message": "You are not logged in. Please log in to get access." } // 403 - Authenticated but insufficient permissions { "code": "Forbidden", "message": "You do not have permission to perform this action." } // 400 - Wrong current password { "code": "InvalidCredentials", "message": "Current password is incorrect." } ``` For the full list of possible errors, see [Error Handling](/docs/guides/error-handling/overview). ## Customizing Authentication Routes [#customizing-authentication-routes] * **[Interceptors](/docs/core-concepts/components/interceptors)** — Run logic before or after any auth endpoint * **[Authentication Setup](/docs/core-concepts/authentication/setup)** — Configure JWT, User Model, and RBAC * **[Static RBAC](/docs/core-concepts/authentication/permissions/static)** — Role-based access control via enums * **[Dynamic RBAC](/docs/core-concepts/authentication/permissions/dynamic)** — Runtime-configurable permissions via database * **[Permissions](/docs/core-concepts/authentication/permissions/static)** — Fine-grained access control # Setup Arkos provides a JWT-based authentication and authorization system with Role-Based Access Control (RBAC) that secures your auto-generated Prisma model routes, built-in auth routes, file upload routes, and custom [ArkosRouter](/docs/core-concepts/components/routers) routes out of the box. This page covers the general authentication setup shared across all permission modes and helps you understand how Arkos handles permissions. ## Permission Modes [#permission-modes] Once a user is authenticated, Arkos needs to know what they're allowed to do. It handles this through two permission modes — [Static](/docs/core-concepts/authentication/permissions/static) and [Dynamic](/docs/core-concepts/authentication/permissions/dynamic). ### Static [#static] [Static mode](/docs/core-concepts/authentication/permissions/static) defines permissions in code via [`ArkosPolicy`](/docs/core-concepts/authentication/permissions/static#arkospolicy) (v1.6+) or [`.auth.ts` files](/docs/core-concepts/authentication/permissions/static#auth-files). Roles are assigned directly on the `User` model as an enum field. Use Static when your roles are stable and known at deploy time — most apps start here. ### Dynamic [#dynamic] [Dynamic mode](/docs/core-concepts/authentication/permissions/dynamic) stores permissions in the database via `AuthRole`, `AuthPermission`, and `UserRole` models. Roles and permissions can be created, updated, and assigned at runtime without a redeploy. Use Dynamic when permissions need to be managed at runtime — multi-tenant apps, SaaS platforms, or any system where roles change frequently. | | [Static](/docs/core-concepts/authentication/permissions/static) | [Dynamic](/docs/core-concepts/authentication/permissions/dynamic) | | ---------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------- | | Permissions defined in | Code | Database | | Role changes require | Redeploy | Database update | | Best for | Stable, predictable roles | Runtime-configurable permissions | | FGAC support | ✅ | ✅ | Both modes share the same config, user model, auth endpoints, and `ArkosPolicy` API — the only difference is where permissions are enforced. ## Configuration [#configuration] Set JWT settings via environment variables — the recommended approach: ```text title=".env" JWT_SECRET=your-super-secret-jwt-key-here JWT_EXPIRES_IN=30d JWT_COOKIE_SECURE=true JWT_COOKIE_HTTP_ONLY=true JWT_COOKIE_SAME_SITE=none ``` Arkos picks these up automatically. If you prefer to be explicit, wire them into your config: ```ts title="arkos.config.ts" import { defineConfig } from "arkos"; export default defineConfig({ authentication: { mode: "static", // or "dynamic" login: { sendAccessTokenThrough: "both", allowedUsernames: ["username"], }, jwt: { secret: process.env.JWT_SECRET, expiresIn: process.env.JWT_EXPIRES_IN || "30d", cookie: { secure: process.env.JWT_COOKIE_SECURE === "true", httpOnly: process.env.JWT_COOKIE_HTTP_ONLY !== "false", sameSite: process.env.JWT_COOKIE_SAME_SITE as "lax" | "strict" | "none", }, }, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { authentication: { mode: "static", login: { sendAccessTokenThrough: "both", allowedUsernames: ["username"], }, jwt: { secret: process.env.JWT_SECRET, expiresIn: process.env.JWT_EXPIRES_IN || "30d", cookie: { secure: process.env.JWT_COOKIE_SECURE === "true", httpOnly: process.env.JWT_COOKIE_HTTP_ONLY !== "false", sameSite: process.env.JWT_COOKIE_SAME_SITE as "lax" | "strict" | "none", }, }, }, }; export default arkosConfig; ``` ```ts title="src/app.ts" import arkos from "arkos"; arkos.init({ authentication: { mode: "static", login: { sendAccessTokenThrough: "both", allowedUsernames: ["username"], }, jwt: { secret: process.env.JWT_SECRET, expiresIn: process.env.JWT_EXPIRES_IN || "30d", cookie: { secure: process.env.JWT_COOKIE_SECURE === "true", httpOnly: process.env.JWT_COOKIE_HTTP_ONLY !== "false", sameSite: process.env.JWT_COOKIE_SAME_SITE as "lax" | "strict" | "none", }, }, }, }); ``` | Option | Env Variable | Default | Description | | ------------------------------ | ---------------------- | --------------------------- | -------------------------------------------------- | | `jwt.secret` | `JWT_SECRET` | — | Signs and verifies tokens — required in production | | `jwt.expiresIn` | `JWT_EXPIRES_IN` | `"30d"` | Token lifetime e.g. `"30d"`, `"1h"` | | `jwt.cookie.secure` | `JWT_COOKIE_SECURE` | `true` in prod | HTTPS-only cookie | | `jwt.cookie.httpOnly` | `JWT_COOKIE_HTTP_ONLY` | `true` | Blocks JS access to cookie | | `jwt.cookie.sameSite` | `JWT_COOKIE_SAME_SITE` | `"lax"` dev / `"none"` prod | SameSite cookie policy | | `login.sendAccessTokenThrough` | — | `"both"` | `"cookie-only"` \| `"response-only"` \| `"both"` | | `login.allowedUsernames` | — | `["username"]` | User model fields accepted as login identifiers | Always set a strong `JWT_SECRET` in production. Arkos throws on login attempts when no secret is configured. ## User Model [#user-model] Arkos requires a `User` model with specific fields in your Prisma schema: ```prisma title="prisma/schema.prisma" // Only needed for Static mode enum UserRole { Admin Editor User } model User { // Required by Arkos id String @id @default(uuid()) username String @unique password String passwordChangedAt DateTime? lastLoginAt DateTime? isSuperUser Boolean @default(false) isStaff Boolean @default(false) deletedSelfAccountAt DateTime? isActive Boolean @default(true) // Static mode — pick one role UserRole @default(User) // single role // roles UserRole[] // multiple roles // Your own fields email String? @unique firstName String? lastName String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } ``` | Field | Purpose | | ---------------------- | -------------------------------------------------------------- | | `username` | Primary login identifier — customizable via `allowedUsernames` | | `password` | Auto-hashed with bcrypt | | `passwordChangedAt` | Invalidates tokens issued before a password change | | `lastLoginAt` | Updated on every successful login | | `isSuperUser` | Bypasses all permission checks — full system access | | `isStaff` | Frontend-only flag for admin area visibility | | `deletedSelfAccountAt` | Soft-deletion timestamp | | `isActive` | When `false`, blocks all access for that user | Create at least one user with `isSuperUser: true` before enabling authentication. By default Arkos requires authentication on all endpoints and only super users have access until permissions are configured. `role` / `roles` is for [Static mode](/docs/core-concepts/authentication/permissions/static) only. [Dynamic mode](/docs/core-concepts/authentication/permissions/dynamic) replaces it with database-driven role relations. ## Protecting Routes [#protecting-routes] Before diving into permission modes, any route can be protected with `authentication: true` — this simply requires the user to be logged in, regardless of their role or permissions. For role-based access control, see [Static Mode](/docs/core-concepts/authentication/permissions/static) or [Dynamic Mode](/docs/core-concepts/authentication/permissions/dynamic). ```ts title="src/modules/post/post.router.ts" import { ArkosRouter } from "arkos"; import postController from "@/src/modules/post/post.controller"; const router = ArkosRouter(); router.get( { path: "/api/posts/dashboard", authentication: true, }, postController.getDashboard ); export default router; ``` ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; export const hook: RouteHook = { findMany: { authentication: false }, // public createOne: { authentication: true }, // login required updateOne: { authentication: true }, deleteOne: { authentication: true }, }; const router = ArkosRouter(); export default router; ``` `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](/docs/core-concepts/components/route-hooks) for full details. For role-based or permission-based access control, pick a permission mode: → [Static Mode](/docs/core-concepts/authentication/permissions/static) — define permissions in code via `ArkosPolicy` or `.auth.ts` files → [Dynamic Mode](/docs/core-concepts/authentication/permissions/dynamic) — manage permissions at runtime via database # Controllers A controller handles the request/response logic for a route. In Arkos, controllers are classes exported as singletons. Each method receives an `ArkosRequest` and `ArkosResponse` and is responsible for calling the appropriate service and sending a response. Because `ArkosRouter` wraps all handlers with `catchAsync` automatically, you never need try/catch blocks or to call `next(err)` manually — just throw an `AppError` and the global error handler takes care of the rest. See [Error Handling](/docs/guides/error-handling/usage) for the full guide. ## Creating a Controller [#creating-a-controller] If you are working with Prisma model routes, Arkos provides `BaseController` which already implements `findMany`, `findOne`, `createOne`, `updateOne`, `deleteOne`, `createMany`, `updateMany`, and `deleteMany` — the methods shown above are already built in. See [BaseController for Prisma Model Routes](#basecontroller-for-prisma-model-routes). ```ts title="src/modules/post/post.controller.ts" import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; import { AppError } from "arkos/error-handler"; import postService from "./post.service"; class PostController { async findMany(req: ArkosRequest, res: ArkosResponse) { const posts = await postService.findMany(); res.status(200).json({ status: "success", data: posts }); } async createOne(req: ArkosRequest, res: ArkosResponse) { const post = await postService.createOne(req.body); res.status(201).json({ status: "success", data: post }); } async findOne(req: ArkosRequest, res: ArkosResponse) { const post = await postService.findOne({ where: { id: req.params.id } }); if (!post) throw new AppError("Post not found", 404, "NotFound"); res.status(200).json({ status: "success", data: post }); } } const postController = new PostController(); export default postController; ``` Wire it up in your router: ```ts title="src/modules/post/post.router.ts" import { ArkosRouter } from "arkos"; import postController from "./post.controller"; const postRouter = ArkosRouter(); postRouter.get({ path: "/api/posts" }, postController.findMany); postRouter.get({ path: "/api/posts/:id" }, postController.findOne); postRouter.post({ path: "/api/posts" }, postController.createOne); export default postRouter; ``` ## Scaffold with the CLI [#scaffold-with-the-cli] ```bash arkos generate controller --module post arkos g c -m post ``` ## BaseController for Prisma Model Routes [#basecontroller-for-prisma-model-routes] When working with Prisma model routes, Arkos auto-generates the CRUD handlers. Your controller extends `BaseController`, which already implements `findMany`, `findOne`, `createOne`, `updateOne`, `deleteOne`, `createMany`, `updateMany`, and `deleteMany`. ```ts title="src/modules/post/post.controller.ts" import { BaseController } from "arkos/controllers"; import postService from "./post.service"; class PostController extends BaseController {} const postController = new PostController(); export default postController; ``` You only need to add methods for behavior beyond the built-in CRUD. The generated routes call these methods automatically. ### Overriding Built-in Methods [#overriding-built-in-methods] Override any `BaseController` method to replace the default behavior entirely: ```ts title="src/modules/post/post.controller.ts" import { BaseController } from "arkos/controllers"; import { ArkosRequest, ArkosResponse } from "arkos"; import postService from "./post.service"; class PostController extends BaseController { async createOne(req: ArkosRequest, res: ArkosResponse) { req.body.authorId = req.user.id; const post = await postService.createOne(req.body); res.status(201).json({ status: "success", data: post }); } } const postController = new PostController(postService); export default postController; ``` If you only need to run logic before or after a built-in operation without replacing it entirely, use [Interceptors](/docs/core-concepts/components/interceptors) instead. Overriding a `BaseController` method drops all built-in behavior for that operation. ### Adding Custom Methods [#adding-custom-methods] Add any method beyond the built-in CRUD and mount it on a custom route: ```ts title="src/modules/post/post.controller.ts" import { BaseController } from "arkos/controllers"; import { ArkosRequest, ArkosResponse } from "arkos"; import postService from "./post.service"; class PostController extends BaseController { async getFeatured(req: ArkosRequest, res: ArkosResponse) { const posts = await postService.findMany({ where: { featured: true } }); res.status(200).json({ status: "success", data: posts }); } } const postController = new PostController(postService); export default postController; ``` ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import postController from "./post.controller"; export const hook: RouteHook = { findMany: { authentication: false }, }; const postRouter = ArkosRouter(); postRouter.get( { path: "/api/posts/featured", authentication: false }, postController.getFeatured ); export default postRouter; ``` ## Related [#related] * [Routers](/docs/core-concepts/components/routers) — Mount controller methods on routes * [Interceptors](/docs/core-concepts/components/interceptors) — Hook into built-in route logic without overriding * [Services](/docs/core-concepts/components/services) — Business logic layer * [Error Handling](/docs/guides/error-handling/usage) — How errors flow through Arkos # Gateways A Gateway is the WebSocket counterpart to a Router. Where a Router exposes HTTP endpoints, a Gateway exposes Socket.io namespace event handlers, with the same building blocks you already use across Arkos: validation, authentication, authorization, and interceptor-style middlewares. ```ts import { ArkosGateway } from "arkos/websockets"; const chatGateway = ArkosGateway({ name: "chat", authentication: true, }); chatGateway.on({ event: "send_message" }, (socket, data) => { socket.to(data.room).emit("receive_message", data); }); export default chatGateway; ``` A Gateway is created with `ArkosGateway(config)` and wired into a `socket.io` `Server` with `.register(io)`. Everything else — defining events, socket enhancements, pipes, hooks, deduplication — lives under [WebSockets](/docs/guides/websockets/setup). ## Anatomy [#anatomy] | Piece | Purpose | | --------------- | --------------------------------------------------------------------- | | `config.name` | Socket.io namespace. Defaults to `"web-socket"`. | | `.on()` | Registers an event handler. | | `.use()` | Registers connection middleware or composes a child Gateway. | | `.pipe()` | Registers event-level middleware. | | `.hook()` | Registers a `connection`, `disconnect`, or `error` lifecycle handler. | | `.register(io)` | Mounts the Gateway (and any nested Gateways) onto a Socket.io server. | ## Where it lives [#where-it-lives] Gateways don't get auto-discovered like Routers do off your Prisma models — you define and register them explicitly, same as a custom Router: ```ts // src/app.ts import { Server } from "socket.io"; import chatGateway from "./gateways/chat.gateway"; const io = new Server(httpServer); chatGateway.register(io); ``` See [WebSockets → Setup](/docs/guides/websockets/setup) for the full registration flow, including plugging in a custom store for rate limiting and deduplication. # Interceptors Interceptors let you run custom logic before, after, or on error of any auto-generated endpoint — Prisma model routes, authentication routes, and file upload routes — without replacing the built-in behavior. Interceptors work alongside [Route Hook](/docs/core-concepts/components/route-hooks) for customizing built-in routes (just like `ArkosRouter`) and give a complete experience when interacting with them. ## Why Interceptors? [#why-interceptors] Arkos auto-generates routes for your Prisma models, authentication, and file uploads. But most applications need custom business logic — setting default values, logging activity, sending notifications, cleaning up resources on failure. Interceptors are the answer. They let you inject your own logic into the auto-generated flow without rewriting the built-in handlers. ## The Three Hook Types [#the-three-hook-types] ### Before Interceptors [#before-interceptors] Run **before** the main operation. Use them to: * Modify request data (`req.body`, `req.query`, `req.params`) * Validate business rules * Check permissions beyond role-based access * Add default values ```ts export const beforeCreateOne = [ async (req, res, next) => { req.body.authorId = req.user.id; next(); }, ]; ``` ### After Interceptors [#after-interceptors] Run **after** the main operation succeeds. Use them to: * Access the result via `res.locals.data.data` * Send notifications, emails, or webhooks * Log successful operations * Transform response data ```ts export const afterCreateOne = [ async (req, res, next) => { const record = res.locals.data.data; await emailService.sendWelcome(record.email); next(); }, ]; ``` ### OnError Interceptors [#onerror-interceptors] Run **when** the main operation fails. Use them to: * Clean up uploaded files * Rollback database transactions * Log errors for monitoring * Send failure alerts ```ts export const onCreateOneError = [ async (err, req, res, next) => { if (req.file) await deleteFile(req.file.path); console.error(err.message); next(err); }, ]; ``` ## File Structure [#file-structure] ``` src/modules/post/ ├── post.middlewares.ts # Reusable functions └── post.interceptors.ts # Chains functions to hooks ``` Scaffold interceptor files with the CLI: ```bash npx arkos generate interceptors --module post # shorthand npx arkos g i -m post ``` This creates: * `src/modules/post/post.interceptors.ts` — empty hook chains For authentication and file upload modules, use `--module auth` or `--module file-upload`. ```ts title="src/modules/post/post.middlewares.ts" import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; export const logCreation = async ( req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { console.log("Post created:", res.locals.data.data); next(); }; ``` ```ts title="src/modules/post/post.interceptors.ts" import { logCreation } from "./post.middlewares"; export const afterCreateOne = [logCreation]; ``` ## Prisma Model Interceptors [#prisma-model-interceptors] Intercept any CRUD operation for prisma model request — add default values before create, log activity after update, or clean up files when creation fails. ```ts title="src/modules/post/post.interceptors.ts" import { setAuthor, logCreation, cleanupImage } from "@/src/modules/post/post.middlewares"; export const beforeCreateOne = [setAuthor]; export const afterCreateOne = [logCreation]; export const onCreateOneError = [cleanupImage]; ``` ```ts title="src/modules/post/post.middlewares.ts" import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; import { AppError } from "arkos/error-handler"; export const setAuthor = async ( req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { req.body.authorId = req.user.id; next(); }; export const logCreation = async ( req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { console.log("Post created:", res.locals.data.data); next(); }; export const cleanupImage = async ( err: any, req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { if (req.body.imageUrl) await deleteFile(req.body.imageUrl); next(err); }; ``` ### Available Prisma Model Interceptors [#available-prisma-model-interceptors] All before, after, and error hooks for Prisma model operations — find, create, update, delete, single or bulk. | Hook | Trigger | | ------------------- | ------------------------------------ | | `beforeFindOne` | Before fetching a single record | | `beforeFindMany` | Before fetching multiple records | | `beforeCreateOne` | Before creating a record | | `beforeCreateMany` | Before creating multiple records | | `beforeUpdateOne` | Before updating a record | | `beforeUpdateMany` | Before updating multiple records | | `beforeDeleteOne` | Before deleting a record | | `beforeDeleteMany` | Before deleting multiple records | | `afterFindOne` | After fetching a record | | `afterFindMany` | After fetching records | | `afterCreateOne` | After creating a record | | `afterCreateMany` | After creating multiple records | | `afterUpdateOne` | After updating a record | | `afterUpdateMany` | After updating multiple records | | `afterDeleteOne` | After deleting a record | | `afterDeleteMany` | After deleting multiple records | | `onFindOneError` | When fetching a record fails | | `onFindManyError` | When fetching records fails | | `onCreateOneError` | When creating a record fails | | `onCreateManyError` | When creating multiple records fails | | `onUpdateOneError` | When updating a record fails | | `onUpdateManyError` | When updating multiple records fails | | `onDeleteOneError` | When deleting a record fails | | `onDeleteManyError` | When deleting multiple records fails | ## How To Access Data In After Interceptors [#how-to-access-data-in-after-interceptors] After a successful operation, the result is available at `res.locals.data.data`: ```ts title="src/modules/post/post.middlewares.ts" import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; export const afterCreateOne = [ async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { // The created/updated record const record = res.locals.data.data; // For list operations, it's an array const records = res.locals.data.data; // For bulk operations, metadata is also available const { total, results } = res.locals.data; next(); }, ]; ``` ## Authentication Interceptors [#authentication-interceptors] Intercept login to rate-limit attempts, send welcome emails after signup, or log failed logins for security monitoring. ```ts title="src/modules/auth/auth.interceptors.ts" import { trackLoginAttempt, sendWelcomeEmail, logFailedLogin } from "./auth.middlewares"; export const beforeLogin = [trackLoginAttempt]; export const afterSignup = [sendWelcomeEmail]; export const onLoginError = [logFailedLogin]; ``` ```ts title="src/modules/auth/auth.middlewares.ts" import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; export const trackLoginAttempt = async ( req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { await redis.incr(`login:${req.body.email}`); next(); }; export const sendWelcomeEmail = async ( req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { const user = res.locals.data.data; await emailService.sendWelcome(user.email); next(); }; export const logFailedLogin = async ( err: any, req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { console.error(`Failed login for ${req.body.email}: ${err.message}`); next(err); }; ``` ### Available Authentication Interceptors [#available-authentication-interceptors] Before, after, and error hooks for login, signup, logout, password changes, and user profile endpoints. | Hook | Trigger | | ----------------------- | -------------------------------- | | `beforeLogin` | Before login | | `afterLogin` | After successful login | | `onLoginError` | When login fails | | `beforeSignup` | Before signup | | `afterSignup` | After successful signup | | `onSignupError` | When signup fails | | `beforeLogout` | Before logout | | `afterLogout` | After logout | | `onLogoutError` | When logout fails | | `beforeUpdatePassword` | Before password update | | `afterUpdatePassword` | After password update | | `onUpdatePasswordError` | When password update fails | | `beforeGetMe` | Before fetching current user | | `afterGetMe` | After fetching current user | | `onGetMeError` | When fetching current user fails | | `beforeUpdateMe` | Before updating current user | | `afterUpdateMe` | After updating current user | | `onUpdateMeError` | When updating current user fails | | `beforeDeleteMe` | Before deleting current user | | `afterDeleteMe` | After deleting current user | | `onDeleteMeError` | When deleting current user fails | ## File Upload Interceptors [#file-upload-interceptors] Intercept file uploads to validate references before delete, log upload activity, or check permissions before serving files. ```ts title="src/modules/file-upload/file-upload.interceptors.ts" import { checkReferences, logUpload } from "./file-upload.middlewares"; export const beforeDeleteFile = [checkReferences]; export const afterUploadFile = [logUpload]; ``` ```ts title="src/modules/file-upload/file-upload.middlewares.ts" import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; import { AppError } from "arkos/error-handler"; export const checkReferences = async ( req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { const { fileName } = req.params; if (await db.isReferenced(fileName)) { throw new AppError("File still in use", 400); } next(); }; export const logUpload = async ( req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { console.log("Uploaded:", res.locals.data.urls); next(); }; ``` ### Available File Upload Interceptors [#available-file-upload-interceptors] Before, after, and error hooks for serving, uploading, replacing, and deleting files. | Hook | Trigger | | ------------------ | ---------------------------- | | `beforeFindFile` | Before serving a file | | `beforeUploadFile` | Before uploading a file | | `afterUploadFile` | After successful upload | | `beforeUpdateFile` | Before replacing a file | | `afterUpdateFile` | After successful replacement | | `beforeDeleteFile` | Before deleting a file | | `afterDeleteFile` | After successful deletion | ## Passing Data Between Interceptors [#passing-data-between-interceptors] Use `res.locals` to pass data between interceptors in the same request: ```ts title="src/modules/post/post.middlewares.ts" import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; export const loadOriginal = async ( req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { const post = await postService.findOne({ id: req.params.id }); res.locals.originalPost = post; next(); }; export const notifyChanges = async ( req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { const original = res.locals.originalPost; const updated = res.locals.data.data; if (original.title !== updated.title) { await emailService.notify(original.authorId, "title changed"); } next(); }; ``` ```ts title="src/modules/post/post.interceptors.ts" import { loadOriginal, notifyChanges } from "./post.middlewares"; export const beforeUpdateOne = [loadOriginal]; export const afterUpdateOne = [notifyChanges]; ``` ## Type Safety [#type-safety] Use `ArkosRequest` and `ArkosResponse` generics for fully typed interceptors: ```ts title="src/modules/post/post.middlewares.ts" import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; import { Prisma } from "@prisma/client"; import { ArkosPrismaInput } from "arkos/prisma"; type CreatePostBody = ArkosPrismaInput; export const addDefaults = async ( req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { if (!req.body.publishedAt) { req.body.publishedAt = new Date(); } next(); }; ``` ## Related [#related] * **[Route Hook](/docs/core-concepts/components/route-hooks)** — Configure auto-generated routes * **[ArkosRouter](/docs/core-concepts/components/routers)** — Full configuration object reference * **[Service Hooks](/docs/core-concepts/components/service-hooks)** — Run logic at the service layer # Route Hooks Route Hook is the new name for what was previously exported as `config: RouterConfig`. If you have existing code using `export const config: RouterConfig`, it still works but will log a deprecation warning. Migrating is a one-line change — see [Migration from RouterConfig](#migration-from-routerconfig). Route Hook is an Arkos component that lets you configure auto-generated routes without touching the route definitions themselves. It is a named export from your module's `*.router.ts` file where each key maps to a built-in operation, and each value is the same configuration object accepted by [ArkosRouter](/docs/core-concepts/components/routers). ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; export const hook: RouteHook = { findMany: { authentication: false }, createOne: { authentication: { resource: "post", action: "Create", rule: ["Admin"], }, }, deleteOne: { disabled: true }, }; const router = ArkosRouter(); export default router; ``` ## Configuration Object [#configuration-object] Every key in a Route Hook accepts the same configuration object as [ArkosRouter](/docs/core-concepts/components/routers) routes — authentication, validation, rate limiting, disabled, and more. See the full reference at [ArkosRouter Configuration Object](/docs/reference/arkos-router#configuration-object). ## Prisma Model Route Hook [#prisma-model-route-hook] Configures the auto-generated RESTful endpoints for a Prisma model. The file lives at `src/modules//.router.ts`. | Key | Method | Endpoint | | ------------ | ------ | ------------------- | | `findMany` | GET | `/api/[model]` | | `findOne` | GET | `/api/[model]/:id` | | `createOne` | POST | `/api/[model]` | | `updateOne` | PATCH | `/api/[model]/:id` | | `deleteOne` | DELETE | `/api/[model]/:id` | | `createMany` | POST | `/api/[model]/many` | | `updateMany` | PATCH | `/api/[model]/many` | | `deleteMany` | DELETE | `/api/[model]/many` | ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import postPolicy from "@/src/modules/post/post.policy"; import UpdatePostSchema from "@/src/modules/post/post.schema"; export const hook: RouteHook<"auth"> = { findMany: { authentication: false }, findOne: { authentication: true }, createOne: { authentication: postPolicy.Create }, updateOne: { authentication: postPolicy.Update, validation: { body: UpdatePostSchema }, }, deleteOne: { authentication: postPolicy.Delete }, createMany: { disabled: true }, updateMany: { disabled: true }, deleteMany: { disabled: true }, }; const router = ArkosRouter(); export default router; ``` ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import postAuthConfigs from "@/src/modules/post/post.auth"; import UpdatePostSchema from "@/src/modules/post/post.schema"; export const hook: RouteHook = { findMany: { authentication: false }, findOne: { authentication: true }, createOne: { authentication: { resource: "post", action: "Create", rule: postAuthConfigs.accessControl.Create, }, }, updateOne: { authentication: { resource: "post", action: "Update", rule: postAuthConfigs.accessControl.Update, }, validation: { body: UpdatePostSchema }, }, deleteOne: { authentication: { resource: "post", action: "Delete", rule: postAuthConfigs.accessControl.Delete, }, }, createMany: { disabled: true }, updateMany: { disabled: true }, deleteMany: { disabled: true }, }; const router = ArkosRouter(); export default router; ``` See [Model Routes](/docs/core-concepts/prisma-orm/routes) for the full breakdown of generated endpoints and query capabilities. ## Authentication Route Hook [#authentication-route-hook] Configures the built-in authentication endpoints. The file lives at `src/modules/auth/auth.router.ts`. | Key | Method | Endpoint | | ---------------- | ------ | --------------------------- | | `login` | POST | `/api/auth/login` | | `signup` | POST | `/api/auth/signup` | | `logout` | DELETE | `/api/auth/logout` | | `updatePassword` | POST | `/api/auth/update-password` | | `getMe` | GET | `/api/users/me` | | `updateMe` | PATCH | `/api/users/me` | | `deleteMe` | DELETE | `/api/users/me` | ```ts title="src/modules/auth/auth.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import UpdateMeSchema from "@/src/modules/auth/schemas/update-me.schema"; export const hook: RouteHook<"auth"> = { login: { rateLimit: { windowMs: 15 * 60_000, max: 10 } }, signup: { disabled: true }, getMe: { rateLimit: { windowMs: 15 * 60_000, max: 30 } }, deleteMe: { disabled: true }, updateMe: { validation: { body: UpdateMeSchema } }, updatePassword: { rateLimit: { windowMs: 15 * 60_000, max: 10 } }, }; const router = ArkosRouter(); export default router; ``` See [Authentication Routes](/docs/core-concepts/authentication/routes) for the full breakdown of auth endpoints and request/response shapes. ## File Upload Route Hook [#file-upload-route-hook] Configures the built-in standalone file upload endpoints. The file lives at `src/modules/file-upload/file-upload.router.ts`. | Key | Method | Endpoint | | ------------ | ------ | ---------------------------------- | | `findFile` | GET | `/api/uploads/:fileType/:fileName` | | `uploadFile` | POST | `/api/uploads/:fileType` | | `updateFile` | PATCH | `/api/uploads/:fileType/:fileName` | | `deleteFile` | DELETE | `/api/uploads/:fileType/:fileName` | ```ts title="src/modules/file-upload/file-upload.router.ts" import { ArkosRouter, RouteHook } from "arkos"; export const hook: RouteHook<"file-upload"> = { findFile: { authentication: false }, uploadFile: { authentication: true }, updateFile: { authentication: true }, deleteFile: { disabled: true }, }; const router = ArkosRouter(); export default router; ``` See [File Upload Routes](/docs/guides/file-handling/file-uploads/routes) for the full breakdown of file upload endpoints and request/response shapes. ## Migration from RouterConfig [#migration-from-routerconfig] Since v1.6, `RouteHook` replaces `RouterConfig` as the recommended export name. The old name still works but will log a deprecation warning: ``` [Warn] 10:35:09 `export const config: RouterConfig` in post.router.ts is deprecated. Use `export const hook: RouteHook` instead. ``` Migrating is a one-line change per file: ```ts // before export const config: RouterConfig = { ... }; // after export const hook: RouteHook = { ... }; ``` # Routers A router groups related endpoints and mounts them into your application. Arkos uses `ArkosRouter` — an enhanced Express router that adds validation, authentication, rate limiting, and OpenAPI support directly on route definitions. If you are working with auto-generated routes (Prisma models, auth, file uploads), you configure those through a [Route Hook](/docs/core-concepts/components/route-hooks) instead of defining routes manually. ## Creating a Router [#creating-a-router] ```ts title="src/modules/post/post.router.ts" import { ArkosRouter } from "arkos"; const postRouter = ArkosRouter(); export default postRouter; ``` ## Registering Routers [#registering-routers] Routers must be registered manually. By convention, all routers are imported into `src/router.ts` and passed to your app: ```ts title="src/router.ts" import { ArkosRouter } from "arkos"; import postRouter from "./modules/post/post.router"; import commentRouter from "./modules/comment/comment.router"; const router = ArkosRouter(); router.use([postRouter, commentRouter]); export default router; ``` Then register them under `src/app.ts`: ```ts title="src/app.ts" import arkos from "arkos"; import router from "./router"; arkos.init({ use: [router], }); ``` ```ts title="src/app.ts" import arkos from "arkos"; import router from "./router"; import { json } from "express"; arkos.init({ use: [json(), router], }); ``` ```ts title="src/app.ts" import arkos from "arkos"; import router from "./router"; arkos.init({ routers: { additional: [router], }, }); ``` ## Defining Routes [#defining-routes] `ArkosRouter` accepts a configuration object as the first argument, followed by your handlers: ```ts title="src/modules/post/post.router.ts" import { ArkosRouter } from "arkos"; import postController from "./post.controller"; const postRouter = ArkosRouter(); postRouter.get( { path: "/api/posts/featured" }, postController.getFeatured ); postRouter.post( { path: "/api/posts" }, postController.create ); export default postRouter; ``` ## Scaffold with the CLI [#scaffold-with-the-cli] ```bash arkos generate router --module post arkos g r -m post ``` ## Route Configuration [#route-configuration] Every route method accepts a configuration object as its first argument. For the full configuration reference see [ArkosRouter](/docs/reference/arkos-router). ### Authentication [#authentication] Pass `true` to require a logged-in user, `false` to make the route public, or an `ArkosPolicy` rule for role-based access. For the full authentication guide see [Authentication](/docs/core-concepts/authentication/setup). ```ts import postPolicy from "./post.policy"; postRouter.get( { path: "/api/posts", authentication: false }, postController.findMany ); postRouter.post( { path: "/api/posts", authentication: postPolicy.Create }, postController.create ); postRouter.delete( { path: "/api/posts/:id", authentication: postPolicy.Delete }, postController.deleteOne ); ``` ### Validation [#validation] Pass Zod schemas or class-validator DTOs to validate `body`, `query`, or `params`. For the full validation guide see [Validation](/docs/guides/validation/usage). ```ts title="src/modules/post/post.router.ts" import { ArkosRouter } from "arkos"; import CreatePostSchema from "./schemas/create-post.schema"; import QueryPostSchema from "./schemas/query-post.schema"; import postController from "./post.controller"; const postRouter = ArkosRouter(); postRouter.get( { path: "/api/posts", validation: { query: QueryPostSchema }, }, postController.findMany ); postRouter.post( { path: "/api/posts", validation: { body: CreatePostSchema }, }, postController.create ); export default postRouter; ``` ```ts title="src/modules/post/post.router.ts" import { ArkosRouter } from "arkos"; import CreatePostDto from "./dtos/create-post.dto"; import QueryPostDto from "./dtos/query-post.dto"; import postController from "./post.controller"; const postRouter = ArkosRouter(); postRouter.get( { path: "/api/posts", validation: { query: QueryPostDto }, }, postController.findMany ); postRouter.post( { path: "/api/posts", validation: { body: CreatePostDto }, }, postController.create ); export default postRouter; ``` ### Rate Limiting [#rate-limiting] Limit how frequently a route can be called: ```ts postRouter.post( { path: "/api/posts", rateLimit: { windowMs: 60_000, max: 10 }, }, postController.create ); ``` ### Disabling Routes [#disabling-routes] Set `disabled: true` to turn off a route without removing it: ```ts postRouter.delete( { path: "/api/posts/:id", disabled: true }, postController.deleteOne ); ``` ### OpenAPI [#openapi] Add OpenAPI metadata directly on the route. For the full OpenAPI guide see [OpenAPI Documentation](/docs/guides/open-api-documentation/usage). ```ts postRouter.get( { path: "/api/posts", openapi: { tags: ["Post"], summary: "List all posts", }, }, postController.findMany ); ``` ## Router-Level Options [#router-level-options] Pass options to `ArkosRouter()` to apply defaults across all routes in that router: ```ts const postRouter = ArkosRouter({ prefix: "posts", openapi: { tags: ["Post"] }, }); ``` ## Auto-Generated Routes [#auto-generated-routes] For Prisma model routes, auth routes, and file upload routes, Arkos generates the endpoints automatically. You configure them via a [Route Hook](/docs/core-concepts/components/route-hooks) exported from the same router file: ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import postPolicy from "./post.policy"; export const hook: RouteHook = { findMany: { authentication: false }, createOne: { authentication: postPolicy.Create }, deleteOne: { disabled: true }, }; const postRouter = ArkosRouter(); export default postRouter; ``` `RouteHook` is the new name for `export const config: RouterConfig`. Existing code still works but logs a deprecation warning. See [Route Hooks](/docs/core-concepts/components/route-hooks) for the full reference. ## Related [#related] * [ArkosRouter Reference](/docs/reference/arkos-router) — Full configuration object reference * [Route Hooks](/docs/core-concepts/components/route-hooks) — Configure auto-generated routes * [Controllers](/docs/core-concepts/components/controllers) — Handle request logic * [Validation](/docs/guides/validation/usage) — Validate request data * [Authentication](/docs/core-concepts/authentication/setup) — Protect routes * [OpenAPI Documentation](/docs/guides/open-api-documentation/usage) — Document your API # Service Hooks Service Hooks run logic at the service layer — every time a `BaseService` method is called, whether from an HTTP endpoint or programmatically. Unlike interceptors (HTTP layer), hooks fire for all service calls across your entire application. ## Why Service Hooks? [#why-service-hooks] Interceptors run at the HTTP layer — they only fire when an API endpoint is called. But what about when you call the same service method from a background job, a CLI command, or another service? Those calls would bypass your interceptor logic. Service Hooks solve this. They run at the service layer, every time a `BaseService` method is called — regardless of where the call comes from. ## The Three Hook Types [#the-three-hook-types] ### Before Service Hooks [#before-service-hooks] Run **before** the main operation. Use them to: * Modify input data before it hits the database * Validate business rules that apply everywhere * Add default values (slug, timestamps, etc.) ```ts export const beforeCreateOne = [ async ({ data }) => { if (!data.slug && data.title) { data.slug = data.title.toLowerCase().replace(/\s+/g, "-"); } }, ]; ``` ### After Service Hooks [#after-service-hooks] Run **after** the main operation succeeds. Use them to: * Access the result via `result` * Send notifications, emails, or webhooks * Update related records * Trigger side effects ```ts export const afterCreateOne = [ async ({ result }) => { await emailService.sendWelcome(result.email); }, ]; ``` ### OnError Service Hooks [#onerror-service-hooks] Run **when** the main operation fails. Use them to: * Rollback database transactions * Clean up temporary data * Log errors for monitoring ```ts export const onCreateOneError = [ async ({ error, data }) => { if (data.tempFile) await deleteFile(data.tempFile); logger.error(error); }, ]; ``` ## File Structure [#file-structure] ``` src/modules/post/ ├── post.service.ts # Extend BaseService └── post.hooks.ts # Hook functions ``` Generate both with the CLI: ```bash npx arkos generate service --module post npx arkos generate hooks --module post ``` Or shorthand: ```bash npx arkos g s -m post npx arkos g h -m post ``` ## Creating a Service [#creating-a-service] Extend `BaseService` to add your own methods: ```ts title="src/modules/post/post.service.ts" import { BaseService } from "arkos/services"; class PostService extends BaseService<"post"> { async getPublished() { return this.findMany({ where: { published: true } }); } } export default new PostService("post"); ``` ## Creating Hooks [#creating-hooks] Each hook exports an array of functions that run before, after, or on error of a service operation: ```ts title="src/modules/post/post.hooks.ts" import { BeforeCreateOneHookArgs } from "arkos/services"; import { Prisma } from "@prisma/client"; export const beforeCreateOne = [ async ({ data }: BeforeCreateOneHookArgs) => { if (!data.slug && data.title) { data.slug = data.title.toLowerCase().replace(/\s+/g, "-"); } }, ]; ``` ## Available Hooks [#available-hooks] All available service hooks. | Hook | Trigger | | ------------------- | -------------------------------- | | `beforeFindOne` | Before fetching a single record | | `beforeFindMany` | Before fetching multiple records | | `beforeCreateOne` | Before creating a record | | `beforeCreateMany` | Before creating multiple records | | `beforeUpdateOne` | Before updating a record | | `beforeUpdateMany` | Before updating multiple records | | `beforeDeleteOne` | Before deleting a record | | `beforeDeleteMany` | Before deleting multiple records | | `beforeCount` | Before counting records | | `afterFindOne` | After fetching a record | | `afterFindMany` | After fetching records | | `afterCreateOne` | After creating a record | | `afterCreateMany` | After creating multiple records | | `afterUpdateOne` | After updating a record | | `afterUpdateMany` | After updating multiple records | | `afterDeleteOne` | After deleting a record | | `afterDeleteMany` | After deleting multiple records | | `afterCount` | After counting records | | `onFindOneError` | When fetching fails | | `onFindManyError` | When fetching fails | | `onCreateOneError` | When creation fails | | `onCreateManyError` | When creation fails | | `onUpdateOneError` | When update fails | | `onUpdateManyError` | When update fails | | `onDeleteOneError` | When deletion fails | | `onDeleteManyError` | When deletion fails | | `onCountError` | When counting fails | ## Hook Arguments [#hook-arguments] ```ts // Before hooks beforeCreateOne = [ async ({ data, context, queryOptions }) => { // Modify data before it hits the database }, ]; // After hooks afterCreateOne = [ async ({ result, data, context, queryOptions }) => { // Access result after operation const created = result; }, ]; // Error hooks onCreateOneError = [ async ({ error, data, context, queryOptions }) => { // Clean up when something fails console.error(error); }, ]; ``` **Context contains:** * `user` — Authenticated user * `accessToken` — JWT token * `skip` — Array of hook types to skip * `throwOnError` — Whether to re-throw errors ## Passing Context [#passing-context] Pass context when calling services programmatically: ```ts import postService from "./post.service"; // Hooks receive this context await postService.createOne( { title: "New Post" }, { include: { author: true } }, // query options { user: currentUser } // context — passed to hooks ); ``` ## Skipping Hooks [#skipping-hooks] Skip specific hook types when needed: ```ts await postService.createOne( data, {}, { skip: ["before", "after"], // Skip before and after hooks user: currentUser, } ); ``` ## Example: User Registration [#example-user-registration] ```ts title="src/modules/user/user.hooks.ts" import { BeforeCreateOneHookArgs, AfterCreateOneHookArgs } from "arkos/services"; import { Prisma } from "@prisma/client"; import authService from "../auth/auth.service"; import emailService from "../email/email.service"; export const beforeCreateOne = [ async ({ data }: BeforeCreateOneHookArgs) => { // Hash password if (data.password) { data.password = await authService.hashPassword(data.password); } // Generate username from email if (!data.username && data.email) { data.username = data.email.split("@")[0]; } }, ]; export const afterCreateOne = [ async ({ result }: AfterCreateOneHookArgs) => { // Create default profile await prisma.profile.create({ data: { userId: result.id, displayName: result.username, }, }); // Send welcome email (don't await — non-blocking) emailService.sendWelcome(result.email).catch(console.error); }, ]; ``` ## Service Hooks vs Interceptors [#service-hooks-vs-interceptors] | | Service Hooks | Interceptors | | ------------- | -------------------------------------- | --------------------------------------- | | **Runs on** | All service calls (API + programmatic) | HTTP endpoints only | | **Access to** | Service context, user | Full Express req/res | | **Best for** | Business logic, data validation | Request processing, response formatting | | **File** | `*.hooks.ts` | `*.interceptors.ts` | ## Related [#related] * **[Interceptors](/docs/core-concepts/components/interceptors)** — Run logic at the HTTP layer * **[BaseService](/docs/reference/base-service)** — Full service API reference * **[Route Hook](/docs/core-concepts/components/route-hooks)** — Configure auto-generated routes # Services A service contains your business logic. Controllers call services, and services interact with the database or other external systems. Keeping this separation means your logic stays testable and reusable — the same service method can be called from an HTTP handler, a background job, or another service. ## Creating a Service [#creating-a-service] If you are working with Prisma model routes, Arkos provides `BaseService` which already implements `findMany`, `findOne`, `createOne`, `updateOne`, `deleteOne`, `createMany`, `updateMany`, `deleteMany`, and `count` — the methods shown above are already built in. See [BaseService for Prisma Models](#baseservice-for-prisma-models). ```ts title="src/modules/post/post.service.ts" import postRepository from "./post.repository"; class PostService { async getFeatured() { return postRepository.findMany({ where: { featured: true } }); } async publish(id: string) { return postRepository.update({ where: { id }, data: { publishedAt: new Date() }, }); } } const postService = new PostService(); export default postService; ``` ## BaseService for Prisma Models [#baseservice-for-prisma-models] When your service maps to a Prisma model, extend `BaseService`. It provides `findMany`, `findOne`, `createOne`, `updateOne`, `deleteOne`, `createMany`, `updateMany`, `deleteMany`, and `count` — all wired to Prisma with built-in support for [Service Hooks](/docs/core-concepts/components/service-hooks). ```ts title="src/modules/post/post.service.ts" import { BaseService } from "arkos/services"; class PostService extends BaseService<"post"> {} const postService = new PostService("post"); export default postService; ``` ### Adding Custom Methods [#adding-custom-methods] Extend with your own methods alongside the built-in ones: ```ts title="src/modules/post/post.service.ts" import { BaseService } from "arkos/services"; import { Prisma } from "@prisma/client"; class PostService extends BaseService<"post"> { async getFeatured() { return this.findMany({ where: { featured: true } }); } async publish(id: string) { return this.updateOne( { where: { id } }, { data: { publishedAt: new Date() } } ); } async getStats() { const [total, published] = await Promise.all([ this.count(), this.count({ where: { publishedAt: { not: null } } }), ]); return { total, published }; } } const postService = new PostService("post"); export default postService; ``` ### Calling Services Programmatically [#calling-services-programmatically] `BaseService` methods accept an optional context object as their last argument. This context is passed to [Service Hooks](/docs/core-concepts/components/service-hooks) and carries the authenticated user: ```ts const post = await postService.createOne( { title: "Hello", body: "World" }, { include: { author: true } }, { user: req.user } ); ``` ### Skipping Hooks [#skipping-hooks] When you need to bypass service hooks for a specific call: ```ts await postService.deleteOne( { where: { id } }, {}, { skip: ["before", "after"] } ); ``` ## Scaffold with the CLI [#scaffold-with-the-cli] ```bash arkos generate service --module post arkos g s -m post ``` ## Related [#related] * [Service Hooks](/docs/core-concepts/components/service-hooks) — Run logic on every service call * [Controllers](/docs/core-concepts/components/controllers) — Call services from route handlers * [Interceptors](/docs/core-concepts/components/interceptors) — Run logic at the HTTP layer # Validators Validators ensure incoming request data is correct before it reaches your controller or service. Arkos supports two validation libraries — **Zod** and **class-validator** — and integrates them directly into route definitions so validation runs automatically. ## How Validation Works [#how-validation-works] You attach a schema or DTO to a route's `validation` config. Arkos validates the request against it before calling your handler. If validation fails, Arkos responds with a `400` error automatically — your handler never runs. ```ts postRouter.post( { path: "/api/posts", validation: { body: CreatePostSchema }, }, postController.createOne ); ``` The `validation` object accepts three optional keys: `body`, `query`, and `params`. ## Zod [#zod] ```ts title="src/modules/post/schemas/create-post.schema.ts" import { z } from "zod"; const CreatePostSchema = z.object({ title: z.string().min(1), body: z.string().min(1), published: z.boolean().optional(), }); export type CreatePostSchemaType = z.infer; export default CreatePostSchema; ``` ```ts title="src/modules/post/schemas/query-post.schema.ts" import { z } from "zod"; const QueryPostSchema = z.object({ page: z.coerce.number().optional(), limit: z.coerce.number().max(100).optional(), sort: z.string().optional(), }); export type QueryPostSchemaType = z.infer; export default QueryPostSchema; ``` ```ts title="src/modules/post/schemas/post-params.schema.ts" import { z } from "zod"; const PostParamsSchema = z.object({ id: z.string().min(1), }); export default PostParamsSchema; ``` ## Class Validator [#class-validator] ```ts title="src/modules/post/dtos/create-post.dto.ts" import { IsString, IsNotEmpty, IsBoolean, IsOptional } from "class-validator"; export default class CreatePostDto { @IsNotEmpty() @IsString() title!: string; @IsNotEmpty() @IsString() body!: string; @IsOptional() @IsBoolean() published?: boolean; } ``` ```ts title="src/modules/post/dtos/query-post.dto.ts" import { IsOptional, IsNumber, IsString, Max } from "class-validator"; import { Transform } from "class-transformer"; export default class QueryPostDto { @IsOptional() @IsNumber() @Transform(({ value }) => (value ? Number(value) : undefined)) page?: number; @IsOptional() @IsNumber() @Max(100) @Transform(({ value }) => (value ? Number(value) : undefined)) limit?: number; @IsOptional() @IsString() sort?: string; } ``` ```ts title="src/modules/post/dtos/post-params.dto.ts" import { IsNotEmpty, IsString } from "class-validator"; export default class PostParamsDto { @IsNotEmpty() @IsString() id!: string; } ``` ## Attaching to Routes [#attaching-to-routes] ```ts title="src/modules/post/post.router.ts" import { ArkosRouter } from "arkos"; import CreatePostSchema from "./schemas/create-post.schema"; import QueryPostSchema from "./schemas/query-post.schema"; import postController from "./post.controller"; const postRouter = ArkosRouter(); postRouter.get( { path: "/api/posts", validation: { query: QueryPostSchema }, }, postController.findMany ); postRouter.post( { path: "/api/posts", validation: { body: CreatePostSchema }, }, postController.createOne ); export default postRouter; ``` ```ts title="src/modules/post/post.router.ts" import { ArkosRouter } from "arkos"; import CreatePostDto from "./dtos/create-post.dto"; import QueryPostDto from "./dtos/query-post.dto"; import postController from "./post.controller"; const postRouter = ArkosRouter(); postRouter.get( { path: "/api/posts", validation: { query: QueryPostDto }, }, postController.findMany ); postRouter.post( { path: "/api/posts", validation: { body: CreatePostDto }, }, postController.createOne ); export default postRouter; ``` ## Validation on Built-in Routes [#validation-on-built-in-routes] For auto-generated routes, attach validation through a [Route Hook](/docs/core-concepts/components/route-hooks): ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import CreatePostSchema from "./schemas/create-post.schema"; import UpdatePostSchema from "./schemas/update-post.schema"; export const hook: RouteHook = { createOne: { validation: { body: CreatePostSchema } }, updateOne: { validation: { body: UpdatePostSchema } }, }; const postRouter = ArkosRouter(); export default postRouter; ``` ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import CreatePostDto from "./dtos/create-post.dto"; import UpdatePostDto from "./dtos/update-post.dto"; export const hook: RouteHook = { createOne: { validation: { body: CreatePostDto } }, updateOne: { validation: { body: UpdatePostDto } }, }; const postRouter = ArkosRouter(); export default postRouter; ``` `RouteHook` is the new name for `export const config: RouterConfig`. Existing code still works but logs a deprecation warning. See [Route Hooks](/docs/core-concepts/components/route-hooks) for details. ## Scaffold with the CLI [#scaffold-with-the-cli] Generate schemas or DTOs from your Prisma model automatically: ```bash # Zod arkos generate create-schema --module post arkos generate update-schema --module post arkos generate query-schema --module post # class-validator arkos generate create-dto --module post arkos generate update-dto --module post arkos generate query-dto --module post ``` ## Related [#related] * [Routers](/docs/core-concepts/components/routers) — Attach validators to routes * [Route Hooks](/docs/core-concepts/components/route-hooks) — Attach validators to auto-generated routes * [CLI — Validation](/docs/tooling/cli/code-generation/validation) — Generate schemas and DTOs from your Prisma schema # Custom Queries As Arkos generates [Prisma Model Routes](/docs/core-concepts/prisma-orm/routes), it also allows you to customize the underlying Prisma queries for each operation without handrolling your own controller logic. This is done through a `[model].query.ts` file that Arkos picks up automatically at boot time. ## Setup [#setup] Create a query options file for your model: ```typescript title="src/modules/post/post.query.ts" import { PrismaQueryOptions } from "arkos/prisma"; import { prisma } from "@/src/utils/prisma"; const postQueryOptions: PrismaQueryOptions = { global: { where: { published: true }, orderBy: { createdAt: "desc" }, }, }; export default postQueryOptions; ``` Or generate it with the CLI: ```bash npx arkos generate query-options --module post # shorthand npx arkos g q -m post ``` ## Configuration Keys [#configuration-keys] Options are applied in this priority order — higher overrides lower: 1. Request query parameters (always highest) 2. Individual operation keys (`findMany`, `createOne`, etc.) 3. Grouped operation keys (`find`, `save`, `create`, etc.) 4. `global` ### Grouped Keys [#grouped-keys] | Key | Applies To | | ---------- | ---------------------------------------------------- | | `find` | `findOne`, `findMany` | | `save` | `createOne`, `updateOne`, `createMany`, `updateMany` | | `create` | `createOne`, `createMany` | | `update` | `updateOne`, `updateMany` | | `delete` | `deleteOne`, `deleteMany` | | `saveOne` | `createOne`, `updateOne` | | `saveMany` | `createMany`, `updateMany` | ### Individual Keys [#individual-keys] `findOne`, `findMany`, `createOne`, `updateOne`, `deleteOne`, `createMany`, `updateMany`, `deleteMany` Each key accepts the full Prisma options for that operation — `select`, `include`, `omit`, `where`, `orderBy`, `take`, `skip`, `distinct`. Options are deep-merged at each level, not replaced. Request query parameters always win but non-conflicting options from your config are preserved. ## Examples [#examples] ### Excluding sensitive fields [#excluding-sensitive-fields] ```typescript title="src/modules/user/user.query.ts" import { PrismaQueryOptions } from "arkos/prisma"; import { prisma } from "@/src/utils/prisma"; const userQueryOptions: PrismaQueryOptions = { global: { omit: { password: true, resetToken: true, }, }, }; export default userQueryOptions; ``` ### Different behavior per operation [#different-behavior-per-operation] ```typescript title="src/modules/post/post.query.ts" import { PrismaQueryOptions } from "arkos/prisma"; import { prisma } from "@/src/utils/prisma"; const postQueryOptions: PrismaQueryOptions = { find: { where: { published: true }, orderBy: { createdAt: "desc" }, }, findMany: { take: 10, select: { id: true, title: true, excerpt: true, createdAt: true, }, }, findOne: { include: { author: true, comments: { where: { approved: true }, orderBy: { createdAt: "asc" }, }, tags: true, }, }, save: { include: { author: true, tags: true }, }, }; export default postQueryOptions; ``` ## Related [#related] * [Prisma Model Routes](/docs/core-concepts/prisma-orm/routes) — The generated endpoints these options apply to * [Handling Relations](/docs/core-concepts/prisma-orm/handling-relations) — How Arkos handles relational data * [Interceptors](/docs/core-concepts/components/interceptors) — Run logic before or after any operation # Handling Relations Arkos provides an easier way to interact with Prisma relations through its **Arkos Prisma Input** system — a built-in runtime transformation that automatically converts flattened JSON data into proper Prisma operations like `connect`, `create`, and `update`. This means you can send intuitive data structures from your frontend without manually structuring nested relation objects. For type safety in your custom code, the `ArkosPrismaInput` TypeScript utility type is also available, giving you the same flattened format with full type inference. ## How It Works [#how-it-works] Arkos scans relation fields in your request body and converts them based on the data shape. The exact result differs slightly depending on whether the relation is **single (one-to-one)** or **array (one-to-many)** — most notably for `delete` and `disconnect`. ### Single (one-to-one) relations [#single-one-to-one-relations] | Input Pattern | Operation | Result | | ----------------------------- | ---------- | --------------------------------------------------------- | | `{ id: 5 }` | Connect | `{ connect: { id: 5 } }` | | `{ name: "New Item" }` | Create | `{ create: { name: "..." } }` | | `{ id: 5, name: "Updated" }` | Update | `{ update: { where: { id: 5 }, data: { name: "..." } } }` | | `{ apiAction: "delete" }` | Delete | `{ delete: true }` | | `{ apiAction: "disconnect" }` | Disconnect | `{ disconnect: true }` | For single relations, `delete` and `disconnect` don't need an `id` — there's only one possible related record, so Arkos always resolves them to `{ delete: true }` / `{ disconnect: true }`. This is different from array relations below, where an `id` is required to target the specific item to remove. ### Array (one-to-many) relations [#array-one-to-many-relations] | Input Pattern | Operation | Result | | ------------------------------------ | ---------- | --------------------------------------------------------- | | `{ id: 5 }` | Connect | `{ connect: { id: 5 } }` | | `{ name: "New Item" }` | Create | `{ create: { name: "..." } }` | | `{ id: 5, name: "Updated" }` | Update | `{ update: { where: { id: 5 }, data: { name: "..." } } }` | | `{ id: 5, apiAction: "delete" }` | Delete | `{ deleteMany: { id: { in: [5] } } }` | | `{ id: 5, apiAction: "disconnect" }` | Disconnect | `{ disconnect: [{ id: 5 }] }` | ## Setup [#setup] No configuration needed. Arkos automatically handles relation fields in all auto-generated endpoints. For custom code, you can use the `ArkosPrismaInput` type for type-safe flattened inputs: ```typescript title="src/modules/post/post.controller.ts" import { ArkosPrismaInput } from "arkos/prisma"; import { Prisma } from "@prisma/client"; type CreatePostInput = ArkosPrismaInput; const postData: CreatePostInput = { title: "My Post", author: { id: 1 }, // Auto-converts to connect tags: [ { name: "Technology" }, // Auto-converts to create { id: 5 } // Auto-converts to connect ] }; ``` `ArkosPrismaInput` utility type is available since v1.5.0-beta. For earlier versions, Arkos still handles relations automatically at runtime — this type just adds TypeScript safety. ## Examples [#examples] ### Single (One-to-One) Relations [#single-one-to-one-relations-1] The same flattened input works for one-to-one relations. Below is every operation against a single `profile` relation on `User`: ```typescript title="src/modules/user/user.controller.ts" import { ArkosPrismaInput } from "arkos/prisma"; import { Prisma } from "@prisma/client"; // Connect to an existing profile const connectProfile: ArkosPrismaInput = { profile: { id: 42 }, }; // -> profile: { connect: { id: 42 } } // Create a new profile inline const createProfile: ArkosPrismaInput = { profile: { bio: "Full-stack developer" }, }; // -> profile: { create: { bio: "Full-stack developer" } } // Update the connected profile in place const updateProfile: ArkosPrismaInput = { profile: { id: 42, bio: "Updated bio" }, }; // -> profile: { update: { where: { id: 42 }, data: { bio: "Updated bio" } } } // Delete the connected profile — no id needed const deleteProfile: ArkosPrismaInput = { profile: { apiAction: "delete" }, }; // -> profile: { delete: true } // Disconnect without deleting — no id needed const disconnectProfile: ArkosPrismaInput = { profile: { apiAction: "disconnect" }, }; // -> profile: { disconnect: true } ``` ### Create with Mixed Relations [#create-with-mixed-relations] ```typescript title="src/modules/post/post.controller.ts" const createPost: ArkosPrismaInput = { title: "New Blog Post", content: "This is the content", author: { id: 123 }, // Connect to existing user (single relation) tags: [ { name: "Technology" }, // Create new tag { name: "Programming" }, // Create new tag { id: 5 } // Connect existing tag ] }; ``` ### Update with Mixed Operations [#update-with-mixed-operations] ```typescript title="src/modules/post/post.controller.ts" const updatePost: ArkosPrismaInput = { title: "Updated Title", comments: [ { id: 1, content: "Updated comment" }, // Update existing { content: "New comment" }, // Create new { id: 3, apiAction: "delete" } // Delete — id required for array relations ] }; ``` ### Connect by Unique Fields [#connect-by-unique-fields] ```typescript title="src/modules/order/order.controller.ts" const createOrder: ArkosPrismaInput = { total: 99.99, customer: { email: "customer@example.com" }, // Connect by email (must be @unique), single relation products: [ { sku: "PROD-123" }, // Connect by SKU (must be @unique) { sku: "PROD-456" } ] }; ``` ### Nested Relations [#nested-relations] Arkos handles nested relations recursively, and single and array relations can be nested inside one another at any depth: ```typescript title="src/modules/post/post.controller.ts" const createPost: ArkosPrismaInput = { title: "My Post", author: { id: 1 }, // Single relation on the post itself comments: [ { content: "Great post!", author: { id: 2 } // Single relation nested inside an array relation — connect }, { content: "Thanks", author: { name: "Anonymous", // Single relation nested inside an array relation — create email: "anon@example.com" } } ] }; ``` ## Explicit Operations with `apiAction` [#explicit-operations-with-apiaction] For ambiguous cases, use `apiAction` to specify the operation. The `id` requirement differs by relation shape: ```typescript title="src/modules/user/user.controller.ts" const updateUser: ArkosPrismaInput = { // Single relation — no id needed for delete/disconnect profile: { apiAction: "disconnect" }, // Array relation — id required to target the specific item posts: [ { id: 1, apiAction: "connect" }, // Connect existing { id: 2, title: "Updated", apiAction: "update" }, // Update { id: 3, apiAction: "delete" }, // Delete { id: 4, apiAction: "disconnect" } // Disconnect without deleting ] }; ``` Valid `apiAction` values: `"create"`, `"connect"`, `"update"`, `"delete"`, `"disconnect"` ## Type Safety with Interceptors [#type-safety-with-interceptors] Use `ArkosPrismaInput` with interceptors for type-safe request manipulation: ```typescript title="src/modules/post/post.interceptors.ts" import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; import { Prisma } from "@prisma/client"; import { ArkosPrismaInput } from "arkos/prisma"; type CreatePostBody = ArkosPrismaInput; export const beforeCreateOne = [ async ( req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { // Type-safe access to flattened relations req.body.author = { id: req.user!.id }; // Add defaults to all tags if (req.body.tags) { req.body.tags = req.body.tags.map(tag => ({ ...tag, type: "user-generated" })); } next(); } ]; ``` ## What Arkos Does Not Handle [#what-arkos-does-not-handle] If you manually structure a relation field using Prisma's native format (`connect`, `create`, etc.), Arkos respects your structure and does not transform it. This allows full control when needed: ```typescript // Arkos respects this — no transformation applied const customPost: Prisma.PostCreateInput = { title: "Custom Post", tags: { connect: [{ id: 1 }, { id: 2 }], create: [{ name: "New Tag" }] } }; ``` However, Arkos **does not** recursively transform manually structured fields. If you mix formats: ```json { "subCategory": { "create": { "name": "New Sub Category", "category": { "id": 3 } // ← Arkos won't transform this inner field } } } ``` To work around this, either: 1. Use the flattened format for all levels 2. Write the inner relation in full Prisma format (`{ connect: { id: 3 } }`) ## Related [#related] * [Prisma Model Routes](/docs/core-concepts/prisma-orm/routes) — The endpoints that use this relation handling * [Custom Queries](/docs/core-concepts/prisma-orm/custom-queries) — Default Prisma options per operation * [Interceptors](/docs/core-concepts/components/interceptors) — Run logic before/after operations * [ArkosPrismaInput API Reference](/docs/reference/arkos-prisma-input) — Full type utility documentation # Routes For every model in your Prisma schema, Arkos automatically generates a full set of RESTful endpoints. The model name is converted to kebab-case and pluralized for the route path. **Example:** A `UserProfile` model is accessible at `/api/user-profiles`. | Method | Endpoint | Description | Operation | | ------ | ------------------- | ----------------------- | ------------ | | GET | `/api/[model]` | List records | `findMany` | | POST | `/api/[model]` | Create one record | `createOne` | | GET | `/api/[model]/:id` | Get one record | `findOne` | | PATCH | `/api/[model]/:id` | Update one record | `updateOne` | | DELETE | `/api/[model]/:id` | Delete one record | `deleteOne` | | POST | `/api/[model]/many` | Create multiple records | `createMany` | | PATCH | `/api/[model]/many` | Update multiple records | `updateMany` | | DELETE | `/api/[model]/many` | Delete multiple records | `deleteMany` | All routes are mounted under your configured `globalPrefix` (default: `/api`). See [Configuration](/docs/getting-started/configuration) for more. ## Configuring Model Routes [#configuring-model-routes] Every generated endpoint accepts the same configuration object used in `ArkosRouter` — disable routes, add rate limiting, authentication, and more via the [Route Hook](/docs/core-concepts/components/route-hooks) in your router file `src/modules//.router.ts` where each key maps to the operation name from the table above: ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouterConfig, RouteHook } from "arkos"; import postPolicy from "@/src/modules/post/post.policy"; import UpdateUserSchema from "@/src/modules/post/schemas/post.schema"; export const hook: RouteHook = { findMany: { authentication: false }, findOne: { authentication: true }, createOne: { authentication: postPolicy.Create }, updateOne: { validation: { body: UpdateUserSchema } }, deleteOne: { disabled: true }, createMany: { disabled: true }, updateMany: { disabled: true }, deleteMany: { disabled: true } }; const router = ArkosRouter(); export default router; ``` `RouteHook` is the new name for `export const config: RouterConfig` introduced in v1.6. The old name still works but will log a deprecation warning. See [Route Hook](/docs/core-concepts/components/route-hooks) for the full guide. See the full configuration object reference at [ArkosRouter Configuration Object](/docs/reference/arkos-router#configuration-object). ## Intercepting Model Requests [#intercepting-model-requests] Every generated endpoint can be intercepted — run logic before or after any operation without replacing the built-in behavior. For example, to log every time a post is created: ```typescript title="@/src/modules/post/post.interceptors.ts" import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; export const afterCreateOne = [ async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { console.log("Post created:", res.locals.data); next(); }, ]; ``` See [Prisma Model Interceptors](/docs/core-concepts/components/interceptors#prisma-model-interceptors) for the full list of available hooks and what you can do with them. ## Sending Model Requests [#sending-model-requests] The examples below use a `Post` model. Requests that require authentication pass the token via the `Authorization` header — it can also be sent through cookies, which are set automatically on login depending on your configuration. ### Find Many Posts [#find-many-posts] ```http GET /api/posts Authorization: Bearer YOUR_API_TOKEN ``` Response: ```json { "total": 42, "results": 10, "data": [ { "id": "1", "title": "Getting Started with Arkos", "content": "...", "authorId": "123", "published": true, "createdAt": "2025-04-05T14:23:45.123Z", "updatedAt": "2025-04-05T14:23:45.123Z" } ] } ``` ### Find One Post [#find-one-post] ```http GET /api/posts/1 ``` Response: ```json { "data": { "id": "1", "title": "Getting Started with Arkos", "content": "...", "authorId": "123", "published": true, "createdAt": "2025-04-05T14:23:45.123Z", "updatedAt": "2025-04-05T14:23:45.123Z" } } ``` ### Create One Post [#create-one-post] ```http POST /api/posts Content-Type: application/json Authorization: Bearer YOUR_API_TOKEN ``` ```json { "title": "My New Post", "content": "Post content here", "authorId": "123", "published": false } ``` Response: ```json { "data": { "id": "42", "title": "My New Post", "content": "Post content here", "authorId": "123", "published": false, "createdAt": "2025-04-05T14:23:45.123Z", "updatedAt": "2025-04-05T14:23:45.123Z" } } ``` ### Create Multiple Posts [#create-multiple-posts] ```http POST /api/posts/many Content-Type: application/json Authorization: Bearer YOUR_API_TOKEN ``` ```json [ { "title": "First Post", "content": "...", "authorId": "123" }, { "title": "Second Post", "content": "...", "authorId": "123" } ] ``` Response: ```json { "total": 2, "results": 2, "data": [ { "id": "43", "title": "First Post", "content": "...", "authorId": "123", "createdAt": "2025-04-05T14:23:45.123Z", "updatedAt": "2025-04-05T14:23:45.123Z" }, { "id": "44", "title": "Second Post", "content": "...", "authorId": "123", "createdAt": "2025-04-05T14:23:45.123Z", "updatedAt": "2025-04-05T14:23:45.123Z" } ] } ``` ### Update One Post [#update-one-post] ```http PATCH /api/posts/1 Content-Type: application/json Authorization: Bearer YOUR_API_TOKEN ``` ```json { "title": "Updated Title", "published": true } ``` Response: ```json { "data": { "id": "1", "title": "Updated Title", "content": "...", "authorId": "123", "published": true, "createdAt": "2025-04-05T14:23:45.123Z", "updatedAt": "2025-04-05T15:30:12.456Z" } } ``` ### Update Multiple Posts [#update-multiple-posts] Filters are passed as query parameters — all matched records are updated with the request body. ```http PATCH /api/posts/many?authorId=123&published=false Content-Type: application/json Authorization: Bearer YOUR_API_TOKEN ``` ```json { "published": true } ``` Response: ```json { "total": 5, "results": 5, "data": [ { "id": "2", "title": "Draft Post", "published": true, "updatedAt": "2025-04-05T15:30:12.456Z" } ] } ``` ### Delete One Post [#delete-one-post] ```http DELETE /api/posts/1 Authorization: Bearer YOUR_API_TOKEN ``` Response: `204 No Content` ### Delete Multiple Posts [#delete-multiple-posts] Filters are passed as query parameters — all matched records are deleted. ```http DELETE /api/posts/many?authorId=123&published=false Authorization: Bearer YOUR_API_TOKEN ``` Response: ```json { "total": 3, "results": 3, "data": [ { "id": "7", "title": "Deleted Draft Post" } ] } ``` Bulk delete requires at least one filter query parameter. Sending `DELETE /api/posts/many` with no filters will return a `400` error to prevent accidental mass deletion. ## Querying [#querying] All list endpoints (`GET /api/[model]`) support a rich set of query parameters that translate directly into Prisma queries. Arkos supports two interchangeable syntaxes — use whichever feels natural, or mix them in the same request: * **Bracket notation:** `price[gte]=50&price[lt]=100` * **Django-style:** `price__gte=50&price__lt=100` ### Filtering [#filtering] Basic filtering matches records where any of the conditions are true (OR by default): ```http GET /api/posts?published=true&authorId=123 ``` To switch to AND logic, use `filterMode`: ```http GET /api/posts?filterMode=AND&published=true&authorId=123 ``` **Comparison operators:** ```http GET /api/products?price[gte]=50&price[lt]=100 # or GET /api/products?price__gte=50&price__lt=100 ``` Supported operators: `gte`, `gt`, `lte`, `lt`, `contains`, `startsWith`, `endsWith`, `in`, `notIn`, and all other Prisma filter operators. **Nested / relational filtering:** ```http GET /api/posts?author[age]=30&comments[some][content][contains]=interesting # or GET /api/posts?author__age=30&comments__some__content__contains=interesting ``` ### Search [#search] Full-text search across all string fields except those ending in `id`, `ids`, `ID`, or `IDs`: ```http GET /api/posts?search=arkos ``` This generates a case-insensitive `contains` check across every eligible string field on the model. ### Sorting [#sorting] Prefix a field with `-` for descending order. Comma-separate multiple fields: ```http GET /api/posts?sort=-createdAt,title ``` This sorts by `createdAt` descending, then `title` ascending. ### Pagination [#pagination] ```http GET /api/posts?page=2&limit=20 ``` `limit` defaults to `30`. The response always includes `total` (all matching records) and `results` (records returned in this page). ### Field Selection [#field-selection] **Select specific fields:** ```http GET /api/posts?fields=id,title,published ``` **Include a relation (adds to default fields):** ```http GET /api/posts?fields=+author ``` **Exclude specific fields:** ```http GET /api/posts?fields=-content,-updatedAt ``` You can combine these in a single request: ```http GET /api/posts?fields=id,title,+author,-updatedAt ``` ### Combining Parameters [#combining-parameters] All query parameters work together: ```http GET /api/posts?search=arkos&published=true&sort=-createdAt&page=1&limit=10&fields=id,title,+author ``` ## Error Responses [#error-responses] ```json // 404 - Record not found { "code": "PostNotFound", "message": "Post with id '999' not found." } // 400 - Missing filters on bulk operation { "code": "MissingFilterCriteria", "message": "Filter criteria not provided for bulk deletion." } ``` For the full list of possible errors, see [Error Handling](/docs/guides/error-handling/overview). ## Customizing Model Routes [#customizing-model-routes] * **[Interceptors](/docs/core-concepts/components/interceptors)** — Run logic before or after any generated endpoint * **[Authentication](/docs/core-concepts/authentication/setup)** — Control which endpoints require authentication and which roles can access them * **[Validation](/docs/guides/validation/setup)** — Add request body and query parameter validation # Setup Arkos.js integrates seamlessly with Prisma ORM, providing auto-generated RESTful endpoints for your models, enhanced type inference, and built-in query capabilities. ## Prerequisites [#prerequisites] Before setting up Prisma with Arkos, ensure you have: * A database connection string ready (PostgreSQL, MySQL, MongoDB, SQLite, etc.) * Arkos.js installed in your project (see [Installation](/docs/getting-started/installation)) ## Setting Up Prisma [#setting-up-prisma] ### 1. Install Prisma Dependencies [#1-install-prisma-dependencies] ```bash npm install @prisma/client npm install --save-dev prisma ``` ```bash pnpm add @prisma/client pnpm add -D prisma ``` ### 2. Initialize Prisma [#2-initialize-prisma] ```bash npx prisma init ``` This creates: * `prisma/schema.prisma` — Your database schema * `.env` — Environment variables (if not exists) ### 2.5. Generate a Model (Optional) [#25-generate-a-model-optional] Instead of manually writing Prisma models, you can use the Arkos CLI to generate one: ```bash npx arkos generate model --module new-model-name # or shorthand: npx arkos g m -m new-model-name ``` This creates the Prisma model with basic fields common along all other existing models or simply basic one. See the [Code Generation Guide](/docs/tooling/cli/code-generation/core) for all options. ### 3. Configure Prisma Schema [#3-configure-prisma-schema] Edit `prisma/schema.prisma` to point to your database and define models: ```prisma title="prisma/schema.prisma" generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" // or mysql, mongodb, sqlite, etc. url = env("DATABASE_URL") } model User { id String @id @default(cuid()) email String @unique name String? posts Post[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Post { id String @id @default(cuid()) title String content String? published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } ``` ### 4. Configure Database Connection [#4-configure-database-connection] Add your database connection string to `.env`: ```text title=".env" DATABASE_URL="postgresql://username:password@localhost:5432/mydb" ``` ### 5. Create Prisma Client Instance [#5-create-prisma-client-instance] Arkos requires your Prisma client to be exported as default from `src/utils/prisma/index.ts` (or `.js` for JavaScript): ```typescript title="src/utils/prisma/index.ts" import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); export default prisma; ``` Arkos dynamically imports your Prisma client from this exact path. The framework uses it to generate model routes, power the `BaseService` class, and enable type inference across your application. ### 6. Generate Prisma Client and Arkos Types [#6-generate-prisma-client-and-arkos-types] ```bash npx arkos prisma generate ``` Available since v1.4.0. This command generates both the Prisma client and the enhanced Arkos type definitions in one go. Run it after any changes to your Prisma schema to keep types up to date. ## Database Initialization [#database-initialization] After setup, initialize your database: ```bash npx prisma db push ``` Pushes your Prisma schema directly to the database without creating migrations. ```bash npx prisma migrate dev --name init ``` Creates a migration file and applies it to your database. ## Next Steps [#next-steps] Now that Prisma is set up with Arkos: * **Create modules** — Add a [module directory](/docs/getting-started/project-structure#modules-directory) for each model you want to customize * **Configure authentication** — Set up [access control](/docs/core-concepts/authentication/setup) for your generated routes * **Add validation** — Define [Zod schemas or class-validator DTOs](/docs/guides/validation/setup) for request bodies * **Customize queries** — Use [default query options](/docs/core-concepts/prisma-orm/custom-queries) to control field selection, includes, and sorting * **Intercept operations** — Add [before/after logic](/docs/core-concepts/components/interceptors) to your model endpoints ## Troubleshooting [#troubleshooting] ### Prisma Client Not Found [#prisma-client-not-found] If you see `Cannot find module '@prisma/client'`, run: ```bash npx arkos prisma generate ``` ### Database Connection Errors [#database-connection-errors] Verify your `DATABASE_URL` in `.env` is correct and the database is accessible. ### Type Errors in TypeScript [#type-errors-in-typescript] Make sure you've run `npx arkos prisma generate` after making changes to your Prisma schema. ### Model Routes Not Generated [#model-routes-not-generated] Check that: * Your Prisma client is exported from `src/utils/prisma/index.ts` * The `prisma` field in `package.json` points to the correct schema location * You have at least one model defined in your Prisma schema # Concepts Arkos is built on the observation that most RESTful APIs spend the majority of their code doing the same things — CRUD endpoints for database models, authentication, and file handling. Not because those problems are hard, but because every project starts from scratch and reinvents the same patterns. Arkos's routing system is the direct answer to that. It gives you two tracks that work together: * **Custom routes** — For everything else, `ArkosRouter` gives you the full power of Express with a declarative configuration layer on top. * **Auto-generated routes** — Arkos reads your Prisma schema, authentication config, and file upload config at boot time and registers a full set of production-ready endpoints automatically. You don't write those routes. Both tracks share the same configuration model. Whether you're configuring a generated route or writing a custom one, the same options — authentication, validation, rate limiting, and more — work identically in both places. ## Custom Routes with ArkosRouter [#custom-routes-with-arkosrouter] For routes that fall outside the generated set, `ArkosRouter` is Arkos's enhanced Express Router. It wraps Express Router with a configuration-first approach — instead of chaining middleware, you declare what a route needs: ```ts title="src/modules/reports/reports.router.ts" import { ArkosRouter } from "arkos"; import reportsPolicy from "@/src/modules/reports/reports.policy"; import { GenerateReportSchema } from "@/src/modules/reports/reports.schema"; import reportsController from "@/src/modules/reports/reports.controller"; const router = ArkosRouter(); router.post( { path: "/reports/generate", authentication: reportsPolicy.Generate, validation: { body: GenerateReportSchema }, rateLimit: { windowMs: 60_000, max: 5 }, }, reportsController.generateReport ); export default router; ``` ## Auto-Generated Routes [#auto-generated-routes] Every model in your Prisma schema gets a full set of RESTful endpoints the moment Arkos boots — no route definitions, no controllers, no boilerplate. A `Post` model immediately has `GET /api/posts`, `POST /api/posts`, `GET /api/posts/:id`, `PATCH /api/posts/:id`, `DELETE /api/posts/:id`, and bulk variants, all backed by a service layer with filtering, sorting, pagination, and field selection built in. The same applies to authentication and file uploads. Once configured, login, signup, logout, password update, file upload, replace, and delete endpoints are all registered automatically. This is Arkos's core philosophy in practice — the things every API needs should not require writing code at all. You configure them, not implement them. The three categories of auto-generated routes are: * [Prisma Model Routes](/docs/core-concepts/prisma-orm/routes) — full CRUD for every model in your schema * [Authentication Routes](/docs/core-concepts/authentication/routes) — login, signup, logout, password update, and current user endpoints * [File Upload Routes](/docs/guides/file-handling/file-uploads/routes) — upload, replace, retrieve, and delete for standalone file operations ## Configuring Generated Routes [#configuring-generated-routes] Auto-generated routes are configured through [Route Hook](/docs/core-concepts/components/route-hooks) — a named export from your module's `*.router.ts` file. Each key maps to a built-in operation and accepts the same configuration object as `ArkosRouter`: ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import postPolicy from "@/src/modules/post/post.policy"; export const hook: RouteHook = { findMany: { authentication: false }, createOne: { authentication: postPolicy.Create }, deleteOne: { disabled: true }, }; const router = ArkosRouter(); export default router; ``` `RouteHook` is the new name for `export const config: RouterConfig` introduced in v1.6. The old name still works but will log a deprecation warning. See [Route Hook](/docs/core-concepts/components/route-hooks) for the full guide. See [ArkosRouter](/docs/reference/arkos-router) for the full configuration object reference. ## Intercepting Generated Routes [#intercepting-generated-routes] Every generated endpoint can be intercepted — run logic before or after any operation without replacing the built-in behavior. Arkos looks for an interceptors file next to the router file: ```ts title="src/modules/auth/auth.interceptors.ts" import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; export const afterSignup = [ async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const user = res.locals.data.data; emailService.sendVerificationEmail(user.email).catch(console.error); next(); }, ]; ``` See [Interceptors](/docs/core-concepts/components/interceptors) for the full list of available hooks per route category. ## Boot Order [#boot-order] At boot time, Arkos registers everything in this order: 1. Built-in middleware (body parser, CORS, compression, security headers, etc.) 2. Your custom routers and middleware 3. Auto-generated routes (Prisma model, authentication, file upload) Your custom routes always take precedence over generated ones. If you define `POST /api/posts` in your own router, it will be reached before Arkos's generated version. ## What's Next [#whats-next] * [ArkosRouter](/docs/reference/arkos-router) — Full configuration object reference * [Route Hook](/docs/core-concepts/components/route-hooks) — Configure auto-generated routes * [Interceptors](/docs/core-concepts/components/interceptors) — Hook into generated route lifecycle # Generated Routes When Arkos boots, it reads your Prisma schema, authentication configuration, and file upload configuration to automatically register three categories of routes — [Prisma Model Routes](/docs/core-concepts/prisma-orm/routes), [Authentication Routes](/docs/core-concepts/authentication/routes), and [File Upload Routes](/docs/guides/file-handling/file-uploads/routes). No route definitions needed. Every endpoint that comes from these categories is fully functional out of the box, and every one of them can be intercepted, configured, or disabled without touching Arkos internals. ## How It Works [#how-it-works] At boot time, Arkos scans your project and registers routes in this order: 1. Built-in Arkos middleware (body parser, CORS, etc.) 2. Your own routers and middleware Registered via `app.use()` before `app.listen()` Registered via `arkos.init({ use: [] })` 3. Auto-generated routes (model, authentication, file upload) This means your custom routes always take precedence over generated ones. ## Configuring Generated Routes with Route Hook [#configuring-generated-routes-with-route-hook] Every generated route accepts the same configuration object used in `ArkosRouter`. You control this through a `hook` export from the module's router file. `RouteHook` is the new name for `export const config: RouterConfig` introduced in v1.6. The old name still works but will log a deprecation warning. See [Route Hook](/docs/core-concepts/components/route-hooks) for the full guide. For a Prisma model, that file lives at `src/modules//.router.ts`. For authentication and file uploads, Arkos looks for `src/modules/auth/auth.router.ts` and `src/modules/file-upload/file-upload.router.ts` respectively. ### Prisma Model Route Hook [#prisma-model-route-hook] ```typescript title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import postPolicy from "@/src/modules/post/post.policy"; import { UpdatePostSchema } from "@/src/modules/post/post.schema"; export const hook: RouteHook = { findMany: { authentication: false }, findOne: { authentication: true }, createOne: { authentication: postPolicy.Create }, updateOne: { authentication: postPolicy.Update, validation: { body: UpdatePostSchema }, }, deleteOne: { authentication: postPolicy.Delete }, createMany: { disabled: true }, updateMany: { disabled: true }, deleteMany: { disabled: true }, }; const router = ArkosRouter(); export default router; ``` ### Authentication Route Hook [#authentication-route-hook] ```typescript title="src/modules/auth/auth.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import { UpdateMeSchema } from "@/src/modules/auth/auth.schema"; export const hook: RouteHook = { login: { rateLimit: { windowMs: 15 * 60_000, max: 10 } }, signup: { disabled: true }, getMe: { rateLimit: { windowMs: 15 * 60_000, max: 30 } }, deleteMe: { disabled: true }, updateMe: { validation: { body: UpdateMeSchema } }, }; const router = ArkosRouter(); export default router; ``` ### File Upload Route Hook [#file-upload-route-hook] ```typescript title="src/modules/file-upload/file-upload.router.ts" import { ArkosRouter, RouteHook } from "arkos"; export const hook: RouteHook = { findFile: { authentication: false }, uploadFile: { authentication: true }, updateFile: { authentication: true }, deleteFile: { disabled: true }, }; const router = ArkosRouter(); export default router; ``` The router file must export `hook` as a named export and the router instance as the default export. If either convention is not followed, Arkos won't pick up your customizations. You can also add your own endpoints to any of these router files — they'll be mounted under the same base path as the generated routes for that module. ## Intercepting Generated Routes [#intercepting-generated-routes] Every generated endpoint can be intercepted — run logic before or after any operation without replacing the built-in behavior. Arkos looks for an interceptors file next to the router file: ```typescript title="src/modules/post/post.interceptors.ts" import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; export const afterCreateOne = [ async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { console.log("Post created:", res.locals.data); next(); }, ]; ``` The same convention applies to auth and file upload interceptors — the file lives at `auth.interceptors.ts` or `file-upload.interceptors.ts` with the corresponding hook names (`afterLogin`, `beforeUploadFile`, etc.). See [Interceptors](/docs/core-concepts/components/interceptors) for the full list of available hooks per category. ## The Three Categories [#the-three-categories] * **[Prisma Model Routes](/docs/core-concepts/prisma-orm/routes)** — RESTful endpoints generated from your Prisma schema, with built-in filtering, sorting, pagination, field selection, and full-text search. * **[Authentication Routes](/docs/core-concepts/authentication/routes)** — Login, signup, logout, password update, and current user endpoints. * **[File Upload Routes](/docs/guides/file-handling/file-uploads/routes)** — Upload, replace, retrieve, and delete endpoints for standalone file operations. # Setup Arkos's routing system is built on top of Express.js. If you've used Express before, most things will feel familiar. The main thing Arkos adds on top is auto-generated CRUD endpoints from your Prisma models — you don't write those routes at all. This page covers how to initialize your app and define your own custom routes. ## Setting Up Your App [#setting-up-your-app] ```typescript title="src/app.ts" import arkos from "arkos"; import router from "@/src/router"; const app = arkos(); app.use(router); app.listen(); ``` ```typescript title="src/app.ts" import arkos from "arkos"; import router from "@/src/router"; arkos.init({ use: [router], }); ``` ### Custom Server Access and WebSockets [#custom-server-access-and-websockets] If you need access to the underlying HTTP server — for WebSockets, for example: Build the app first with `app.build()`, then create the HTTP server manually and pass it to `app.listen()`: ```typescript title="src/app.ts" import arkos from "arkos"; import http from "http"; import { Server } from "socket.io"; const app = arkos(); await app.build(); const server = http.createServer(app); const io = new Server(server); app.listen(server); ``` Use the `configureServer` callback inside `arkos.init()`: ```typescript title="src/app.ts" import arkos from "arkos"; import { Server } from "socket.io"; await arkos.init({ configureServer: async (server) => { const io = new Server(server); }, }); ``` `app.build()` handles all of Arkos's internal setup — loading Prisma, scanning modules, registering routes. It must be awaited before you create the HTTP server, otherwise built-in routes and middleware won't be in place when connections arrive. In the simple case (`app.listen()` with no arguments), this is done for you automatically. ### Configuring Express [#configuring-express] In v1.6+, `arkos()` returns the Express app directly, so you can configure it before calling `app.listen()`: ```typescript title="src/app.ts" import arkos from "arkos"; import helmet from "helmet"; const app = arkos(); app.set("trust proxy", true); app.use(helmet()); app.listen(); ``` Use the `configureApp` callback inside `arkos.init()`: ```typescript title="src/app.ts" import arkos from "arkos"; import helmet from "helmet"; arkos.init({ configureApp: async (app) => { app.set("trust proxy", true); app.use(helmet()); app.locals.title = "My Arkos Application"; app.set("view engine", "ejs"); app.use((req, res, next) => { next(); }); }, }); ``` ### arkos.config.ts [#arkosconfigts] Port, host, and other global settings live in your config file: ```typescript title="arkos.config.ts" import { defineConfig } from "arkos"; const arkosConfig = defineConfig({ globalPrefix: "/api", port: 3000, host: "localhost", }); export default arkosConfig; ``` ```typescript title="arkos.config.ts" export default { port: 3000, host: "localhost", }; ``` `globalPrefix` is available since v1.6.0-beta. It defaults to `/api`. See [Configuration](/docs/getting-started/configuration) for all available options. ## Custom Routes with ArkosRouter [#custom-routes-with-arkosrouter] For routes you write yourself, Arkos provides `ArkosRouter` — a thin wrapper around Express Router that adds declarative configuration support: ```typescript title="src/router.ts" import { ArkosRouter } from "arkos"; import userRouter from "@/src/modules/user/user.router"; import reportsRouter from "@/src/modules/reports/reports.router"; const router = ArkosRouter(); router.use(userRouter); router.use(reportsRouter); export default router; ``` Each module defines its own router: ```typescript title="src/modules/reports/reports.router.ts" import { ArkosRouter } from "arkos"; import reportsController from "@/src/modules/reports/reports.controller"; import reportsPolicy from "@/src/modules/reports/reports.policy"; import GenerateReportSchema from "@/src/modules/reports/schemas/generate-report.schema"; const router = ArkosRouter(); router.post( { path: "/reports/generate", authentication: reportsPolicy.Generate, validation: { body: GenerateReportSchema }, }, reportsController.generateReport ); export default router; ``` Express Router is supported for backward compatibility but not recommended. Using it means you lose access to built-in authentication, validation, rate limiting, and other features that ArkosRouter provides through its configuration object. ### Route Paths [#route-paths] Paths are defined relative to the `globalPrefix`. With `globalPrefix: "/api"` (the default), a route defined as `/reports/generate` is accessible at `/api/reports/generate`. Paths are absolute. A route defined as `/reports/generate` is accessible at `/reports/generate` with no prefix applied automatically. ## Project Structure [#project-structure] A suggested structure that scales well: ``` src/ ├── router.ts # Single import point for all routers ├── modules/ │ ├── user/ │ │ ├── user.router.ts │ │ ├── user.controller.ts │ │ └── user.service.ts │ └── reports/ │ ├── reports.router.ts │ ├── reports.controller.ts │ └── reports.service.ts ``` ## What's Next [#whats-next] * **[ArkosRouter](/docs/reference/arkos-router)** — Full API reference for the route configuration object * **[Route Hook](/docs/core-concepts/components/route-hooks)** — Configure auto-generated routes * **[Interceptors](/docs/core-concepts/components/interceptors)** — Hook into built-in routes with before/after middleware # Configuration Arkos provides a comprehensive configuration system that lets you customize every aspect of your application. Since v1.4.0, configuration is split between static settings in `arkos.config.ts` and runtime setup in `src/app.ts`. ## Configuration Overview [#configuration-overview] In v1.6+, Arkos uses `defineConfig` for full type safety and IDE autocomplete: ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ middlewares: { cors: { allowedOrigins: "*" }, }, authentication: { mode: "static", }, validation: { resolver: "zod", }, }); ``` ```ts title="src/app.ts" import arkos from "arkos"; import analyticsRouter from "./routers/analytics.router"; const app = arkos(); app.use(analyticsRouter); app.listen(); ``` In v1.4–v1.5, static configuration lives in `arkos.config.ts` and runtime setup in `arkos.init()`: ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { middlewares: { cors: { allowedOrigins: "*" }, }, authentication: { mode: "static", }, validation: { resolver: "zod", }, }; export default arkosConfig; ``` ```ts title="src/app.ts" import arkos from "arkos"; import analyticsRouter from "./routers/analytics.router"; arkos.init({ use: [analyticsRouter], configureApp: (app) => { app.set("trust proxy", 1); }, configureServer: (server) => { server.setTimeout(30000); }, }); ``` In v1.3, all configuration is passed directly to `arkos.init()` in `src/app.ts`: ```ts title="src/app.ts" import arkos from "arkos"; arkos.init({ cors: { allowedOrigins: "*" }, authentication: { mode: "static" }, validation: { resolver: "zod" }, routers: { additional: [customRouter] }, middlewares: { additional: [customMiddleware] }, }); ``` For the full list of all available configuration options see [Arkos Configuration](/docs/reference/arkos-configuration). ## Basic Application Settings [#basic-application-settings] ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ welcomeMessage: "Welcome to My API", port: 8000, host: "localhost", globalPrefix: "/api", }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { welcomeMessage: "Welcome to My API", port: 8000, host: "localhost", }; export default arkosConfig; ``` ```ts title="src/app.ts" arkos.init({ welcomeMessage: "Welcome to My API", port: 8000, host: "localhost", }); ``` ## Authentication [#authentication] ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ authentication: { mode: "static", login: { allowedUsernames: ["email", "username"], sendAccessTokenThrough: "both", }, jwt: { secret: process.env.JWT_SECRET, expiresIn: "30d", cookie: { secure: true, httpOnly: true, sameSite: "lax", }, }, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { authentication: { mode: "static", login: { allowedUsernames: ["email", "username"], sendAccessTokenThrough: "both", }, jwt: { secret: process.env.JWT_SECRET, expiresIn: "30d", cookie: { secure: true, httpOnly: true, sameSite: "lax", }, }, }, }; export default arkosConfig; ``` ```ts title="src/app.ts" arkos.init({ authentication: { mode: "static", login: { allowedUsernames: ["email", "username"], sendAccessTokenThrough: "both", }, jwt: { secret: process.env.JWT_SECRET, expiresIn: "30d", }, }, }); ``` See [Authentication Setup](/docs/core-concepts/authentication/setup) for full details. ## Validation [#validation] ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ validation: { resolver: "zod", // or "class-validator" }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { validation: { resolver: "zod", }, }; export default arkosConfig; ``` ```ts title="src/app.ts" arkos.init({ validation: { resolver: "class-validator", validationOptions: { whitelist: true, forbidNonWhitelisted: true, }, }, }); ``` See [Validation Setup](/docs/guides/validation/setup) for full details. ## File Upload [#file-upload] ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ fileUpload: { baseUploadDir: "./uploads", baseRoute: "/api/files", restrictions: { images: { maxCount: 10, maxSize: 5 * 1024 * 1024, supportedFilesRegex: /\.(jpg|jpeg|png|gif)$/i, }, documents: { maxCount: 5, maxSize: 10 * 1024 * 1024, supportedFilesRegex: /\.(pdf|doc|docx)$/i, }, }, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { fileUpload: { baseUploadDir: "./uploads", baseRoute: "/api/files", restrictions: { images: { maxCount: 10, maxSize: 5 * 1024 * 1024, supportedFilesRegex: /\.(jpg|jpeg|png|gif)$/i, }, }, }, }; export default arkosConfig; ``` ```ts title="src/app.ts" arkos.init({ fileUpload: { baseUploadDir: "./uploads", restrictions: { images: { maxCount: 10, maxSize: 5 * 1024 * 1024, }, }, }, }); ``` See [File Upload Setup](/docs/guides/file-handling/file-uploads/setup) for full details. ## Middlewares [#middlewares] ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ middlewares: { rateLimit: { windowMs: 60 * 1000, limit: 100 }, cors: { allowedOrigins: ["https://myapp.com"], options: { credentials: true }, }, compression: { level: 6 }, expressJson: { limit: "10mb" }, requestLogger: false, }, }); ``` Custom middlewares and routers are registered via `app.use()` in `src/app.ts`: ```ts title="src/app.ts" import arkos from "arkos"; import myMiddleware from "./middlewares/my-middleware"; import analyticsRouter from "./routers/analytics.router"; const app = arkos(); app.use(myMiddleware); app.use(analyticsRouter); app.listen(); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { middlewares: { rateLimit: { windowMs: 60 * 1000, limit: 100 }, cors: { allowedOrigins: ["https://myapp.com"], options: { credentials: true }, }, compression: { level: 6 }, }, }; export default arkosConfig; ``` ```ts title="src/app.ts" import arkos from "arkos"; import myMiddleware from "./middlewares/my-middleware"; arkos.init({ use: [myMiddleware], }); ``` ```ts title="src/app.ts" arkos.init({ middlewares: { additional: [customMiddleware], }, globalRequestRateLimitOptions: { windowMs: 60 * 1000, limit: 100, }, cors: { allowedOrigins: ["https://myapp.com"], }, }); ``` See [Global Middlewares](/docs/guides/global-middlewares) for the full list and options. ## Routers [#routers] ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ routers: { strict: true, welcomeRoute: false, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { routers: { strict: true, welcomeRoute: false, }, }; export default arkosConfig; ``` ```ts title="src/app.ts" arkos.init({ routers: { strict: true, additional: [customRouter], }, }); ``` ## Advanced Runtime Configuration [#advanced-runtime-configuration] These options live in `src/app.ts` regardless of version since they require access to the live Express app or HTTP server: ```ts title="src/app.ts" import arkos from "arkos"; import helmet from "helmet"; const app = arkos(); app.set("trust proxy", 1); app.use(helmet()); app.listen(); ``` For WebSocket support or custom HTTP server access: ```ts title="src/app.ts" import arkos from "arkos"; import http from "http"; import { Server } from "socket.io"; const app = arkos(); async function start() { await app.build(); const server = http.createServer(app); const io = new Server(server); app.listen(server); } start(); ``` ```ts title="src/app.ts" import arkos from "arkos"; arkos.init({ configureApp: (app) => { app.set("trust proxy", 1); app.set("view engine", "ejs"); }, configureServer: (server) => { server.setTimeout(30000); }, }); ``` ```ts title="src/app.ts" import arkos from "arkos"; arkos.init({ configureApp: (app) => { app.set("trust proxy", 1); }, configureServer: (server) => { server.setTimeout(30000); }, }); ``` ## Environment Variables [#environment-variables] Arkos picks up several environment variables automatically: ```bash title=".env" # Database DATABASE_URL=postgresql://username:password@localhost:5432/dbname # JWT JWT_SECRET=your-super-secret-jwt-key JWT_EXPIRES_IN=30d JWT_COOKIE_SECURE=true JWT_COOKIE_HTTP_ONLY=true JWT_COOKIE_SAME_SITE=lax # Server PORT=8000 NODE_ENV=development HOST=localhost # Email EMAIL_HOST=smtp.gmail.com EMAIL_PORT=465 EMAIL_SECURE=true EMAIL_USER=you@example.com EMAIL_PASSWORD=your-smtp-password EMAIL_NAME=Your App ``` Arkos loads environment variables in this order (highest priority first): 1. Process environment variables (system-level) 2. `.env` 3. `.env.local` 4. `.env.[NODE_ENV].local` 5. `.env.[NODE_ENV]` 6. `.env.defaults` ## Complete Example [#complete-example] ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ port: 8000, host: "0.0.0.0", welcomeMessage: "Welcome to Our API", authentication: { mode: "static", jwt: { secret: process.env.JWT_SECRET, expiresIn: "7d", }, }, validation: { resolver: "zod", }, fileUpload: { baseUploadDir: "/uploads", restrictions: { images: { maxCount: 5, maxSize: 5 * 1024 * 1024, }, }, }, middlewares: { cors: { allowedOrigins: ["https://myapp.com"] }, rateLimit: { windowMs: 60000, limit: 500 }, compression: { level: 6 }, }, routers: { strict: "no-bulk", }, email: { host: process.env.EMAIL_HOST, auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASSWORD, }, }, swagger: { mode: "zod", enableAfterBuild: false, }, }); ``` ```ts title="src/app.ts" import arkos from "arkos"; import analyticsRouter from "./routers/analytics.router"; const app = arkos(); app.set("trust proxy", 1); app.use(analyticsRouter); app.listen(); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { port: 8000, host: "0.0.0.0", welcomeMessage: "Welcome to Our API", authentication: { mode: "static", jwt: { secret: process.env.JWT_SECRET, expiresIn: "7d", }, }, validation: { resolver: "zod", }, fileUpload: { baseUploadDir: "/uploads", restrictions: { images: { maxCount: 5, maxSize: 5 * 1024 * 1024, }, }, }, middlewares: { cors: { allowedOrigins: ["https://myapp.com"] }, rateLimit: { windowMs: 60000, limit: 500 }, }, routers: { strict: "no-bulk", }, email: { host: process.env.EMAIL_HOST, auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASSWORD, }, }, }; export default arkosConfig; ``` ```ts title="src/app.ts" import arkos from "arkos"; import analyticsRouter from "./routers/analytics.router"; arkos.init({ use: [analyticsRouter], configureApp: (app) => { app.set("trust proxy", 1); }, configureServer: (server) => { server.setTimeout(30000); }, }); ``` ```ts title="src/app.ts" import arkos from "arkos"; import analyticsRouter from "./routers/analytics.router"; arkos.init({ port: 8000, welcomeMessage: "Welcome to Our API", authentication: { mode: "static", jwt: { secret: process.env.JWT_SECRET, expiresIn: "7d", }, }, validation: { resolver: "zod" }, fileUpload: { baseUploadDir: "/uploads", }, routers: { strict: "no-bulk", additional: [analyticsRouter], }, configureApp: (app) => { app.set("trust proxy", 1); }, }); ``` # Deployment Deploying an Arkos.js application is the same as deploying any standard Node.js application. This guide covers the build process and common deployment setups. ## Prerequisites [#prerequisites] Before deploying, make sure you have: * Node.js 22.9+ on your production environment * All environment variables configured * A database accessible from the production server ## Building [#building] ```bash pnpm install pnpm run build ``` The build output lands in `.build/`. The production start command runs from there: ```bash pnpm run start ``` ## Environment Variables [#environment-variables] ```bash title=".env" # Required DATABASE_URL=your-production-database-url JWT_SECRET=your-production-jwt-secret # Recommended PORT=8000 HOST=0.0.0.0 NODE_ENV=production JWT_COOKIE_SECURE=true JWT_COOKIE_HTTP_ONLY=true ``` Always set a strong `JWT_SECRET` in production. Arkos throws on login attempts when no secret is configured. ## Database [#database] Run migrations before starting the app: ```bash npx prisma migrate deploy npx prisma generate ``` ## Deployment Platforms [#deployment-platforms] ### VPS (DigitalOcean, Hetzner, Contabo, AWS EC2) [#vps-digitalocean-hetzner-contabo-aws-ec2] ```bash # Install dependencies and build pnpm install pnpm run build # Run with PM2 npm install -g pm2 pm2 start pnpm --name "my-app" -- run start pm2 save pm2 startup ``` A minimal PM2 ecosystem file: ```js title="ecosystem.config.js" module.exports = { apps: [{ name: "my-app", script: "pnpm", args: ["run", "start"], instances: "max", exec_mode: "cluster", env: { NODE_ENV: "production", }, }], }; ``` ### Platform as a Service (Railway, Render, Heroku) [#platform-as-a-service-railway-render-heroku] Set your environment variables in the platform dashboard, then point the start command at `pnpm run start`. For Heroku, add a `Procfile`: ```bash title="Procfile" web: pnpm run start ``` ### Docker [#docker] ```dockerfile title="Dockerfile" FROM node:22-alpine WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN npm install -g pnpm && pnpm install COPY . . RUN pnpm run build RUN npx prisma generate EXPOSE 8000 CMD ["pnpm", "run", "start"] ``` ```yaml title="docker-compose.yml" version: "3.8" services: app: build: . ports: - "8000:8000" environment: - DATABASE_URL=postgresql://user:pass@db:5432/arkos - JWT_SECRET=your-jwt-secret - NODE_ENV=production depends_on: - db db: image: postgres:16-alpine environment: - POSTGRES_DB=arkos - POSTGRES_USER=user - POSTGRES_PASSWORD=pass volumes: - postgres_data:/var/lib/postgresql/data volumes: postgres_data: ``` ## Nginx Reverse Proxy [#nginx-reverse-proxy] ```nginx server { listen 80; server_name your-domain.com; location / { proxy_pass http://localhost:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_cache_bypass $http_upgrade; } } ``` Set `app.set("trust proxy", 1)` in `src/app.ts` when running behind a reverse proxy so that rate limiting and IP-based features work correctly. ## CI/CD Example [#cicd-example] ```yaml title=".github/workflows/deploy.yml" name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "22" - name: Install pnpm uses: pnpm/action-setup@v3 with: version: latest - name: Install and build run: | pnpm install pnpm run build - name: Run tests run: pnpm test - name: Deploy # Add your deployment step here ``` ## File Uploads in Production [#file-uploads-in-production] Local file storage works fine for single-server deployments. For multi-instance or containerized setups, use cloud storage (S3, Cloudinary, R2) instead — files written to the local filesystem won't be shared across instances. # Installation On this guide you will learn how to install and create an Arkos.js project. ## System Requirements [#system-requirements] Before you begin, make sure your system meets the following requirements: * [**Node.js**](https://nodejs.org) 22.9 or later * macOS, Windows, or Linux ## Quick Start [#quick-start] The quickest way to create a new Arkos.js RESTful API is using [**create-arkos**](/docs/tooling/create-arkos), which sets up everything automatically for you. ### 1. Run `create-arkos` [#1-run-create-arkos] ```bash pnpm create arkos@latest ``` ```bash npm create arkos@latest ``` On installation, you'll see the following prompts: ```bash ? What is the name of your project? my-project ? Would you like to use TypeScript? Yes ? What db provider will be used for Prisma? postgresql ? Would you like to set up Validation? Yes ? Choose validation library: zod ? Would you like to set up Authentication? Yes ? Choose authentication type: static ? Choose default username field for login: email ? Would you like to use authentication with Multiple Roles? Yes ``` After the prompts, `create-arkos` will create a folder with your project name, install all required dependencies, and run `npx prisma generate` to generate your first `@prisma/client`. ### 2. Setup Environment Variables [#2-setup-environment-variables] Add `DATABASE_URL` and `JWT_SECRET` to the `.env` file that was generated: ```bash title=".env" DATABASE_URL=postgresql://username:password@localhost:5432/my-project JWT_SECRET=my-super-secret-key PORT=8000 ``` ### 3. Initialize Database [#3-initialize-database] Once your `DATABASE_URL` is set, initialize your database with Prisma: ```bash npx prisma db push ``` Or if you prefer migrations: ```bash npx prisma migrate dev --name init ``` ### 4. Generate Arkos and Prisma Client Types [#4-generate-arkos-and-prisma-client-types] Required since v1.4.0-beta. Improves the TypeScript experience when using `BaseService` classes. ```bash npx arkos prisma generate ``` ### 5. Run The Dev Server [#5-run-the-dev-server] After these steps you're ready to start developing. ```bash pnpm run dev ``` ```bash npm run dev ``` Your application will be running at `http://localhost:8000/api` or on the `PORT` you set in `.env`. ## Manual Installation [#manual-installation] ### 1. Initialize Your Project [#1-initialize-your-project] ```bash mkdir my-arkos-project cd my-arkos-project pnpm init ``` ### 2. Install Required Packages [#2-install-required-packages] ```bash pnpm add arkos @prisma/client pnpm add -D typescript @types/node prisma tsx-strict ``` ```bash pnpm add arkos @prisma/client pnpm add -D prisma tsx-strict ``` ### 3. Configure Environment Variables [#3-configure-environment-variables] Create your `.env` file: ```bash title=".env" DATABASE_URL=your-database-connection-string JWT_SECRET=your-secret-key-for-authentication PORT=8000 ``` ### 4. Set Up Prisma Schema [#4-set-up-prisma-schema] Create `prisma/schema.prisma`: ```prisma title="prisma/schema.prisma" generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" // or mysql, mongodb, sqlite, etc. url = env("DATABASE_URL") } ``` ### 5. Export Your Prisma Client [#5-export-your-prisma-client] Arkos requires your Prisma client to be exported as default from `src/utils/prisma/index.ts` so it can dynamically import it to manage auto-generated endpoints and power the [BaseService](/docs/reference/base-service) class. ```ts title="src/utils/prisma/index.ts" import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); export default prisma; ``` ### 6. Configure Your Application [#6-configure-your-application] ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ middlewares: { cors: { allowedOrigins: "*", }, }, }); ``` ```ts title="src/app.ts" import arkos from "arkos"; const app = arkos(); app.listen(); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { middlewares: { cors: { allowedOrigins: "*", }, }, }; export default arkosConfig; ``` ```ts title="src/app.ts" import arkos from "arkos"; arkos.init(); ``` ```ts title="src/app.ts" import arkos from "arkos"; arkos.init({ middlewares: { cors: { allowedOrigins: "*", }, }, }); ``` ### 7. Set Up Package.json Scripts [#7-set-up-packagejson-scripts] ```json title="package.json" { "type": "module", "scripts": { "dev": "arkos dev", "build": "arkos build", "start": "arkos start", "arkos": "arkos" }, "prisma": { "schema": "prisma/schema/" } } ``` * `dev` — Starts the development server with hot reloading * `build` — Builds your application for production * `start` — Runs the built application in production mode * `arkos` — Provides access to [Arkos CLI](/docs/tooling/cli) commands ### 8. Initialize Database [#8-initialize-database] ```bash npx prisma generate npx prisma db push ``` ### 9. Generate Arkos and Prisma Client Types [#9-generate-arkos-and-prisma-client-types] ```bash npx arkos prisma generate ``` ### 10. Start Development Server [#10-start-development-server] ```bash pnpm run dev ``` ```bash npm run dev ``` Your API is now running at `http://localhost:8000/api`. From here, refer to [Project Structure](/docs/getting-started/project-structure) to understand how to organize your code, and [Configuration](/docs/getting-started/configuration) for all available options. # 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 [#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 [#application-entry-point] ```ts title="src/app.ts" import arkos from "arkos"; import analyticsRouter from "@/src/modules/analytics/analytics.router"; const app = arkos(); app.use(analyticsRouter); app.listen(); ``` ```ts title="src/app.ts" import arkos from "arkos"; import analyticsRouter from "./routers/analytics.router"; arkos.init({ use: [analyticsRouter], configureApp: (app) => { app.set("trust proxy", 1); }, }); ``` ```ts title="src/app.ts" import arkos from "arkos"; import analyticsRouter from "./routers/analytics.router"; arkos.init({ cors: { allowedOrigins: "*" }, routers: { additional: [analyticsRouter], }, }); ``` ## Prisma Client — Required [#prisma-client--required] Arkos dynamically imports your Prisma client to power auto-generated endpoints and the [BaseService](/docs/reference/base-service) class. It must be exported as default from exactly this path: ```ts title="src/utils/prisma/index.ts" import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); export default prisma; ``` ## Modules Directory [#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`) [#controller-controllerts] For Prisma model routes, extend `BaseController` which already provides `createOne`, `findMany`, `findOne`, `updateOne`, `updateMany`, `deleteOne`, `deleteMany`, and `createMany`. Add custom methods on top: ```ts title="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`) [#service-servicets] Extend `BaseService` to add business logic on top of the built-in CRUD methods: ```ts title="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"); ``` ```ts title="src/modules/post/post.service.ts" import { BaseService } from "arkos/services"; import { Prisma } from "@prisma/client"; class PostService extends BaseService { async getPublished() { return this.findMany({ where: { published: true } }); } } export default new PostService("post"); ``` ### Router (`*.router.ts`) [#router-routerts] Defines custom routes and configures built-in auto-generated routes via `RouteHook`: ```ts title="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](/docs/core-concepts/components/route-hooks) for full details. ### Interceptors (`*.interceptors.ts`) [#interceptors-interceptorsts] 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: ```ts title="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](/docs/core-concepts/components/interceptors) for the full list of available hooks. ### Policy (`*.policy.ts`) [#policy-policyts] Defines permissions for a module using `ArkosPolicy` (v1.6+, recommended): ```ts title="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](/docs/core-concepts/authentication/permissions/static) for full details. For projects using the older `.auth.ts` approach, see [Auth Config Files](/docs/core-concepts/authentication/permissions/static#auth-config-files). ### Service Hooks (`*.hooks.ts`) [#service-hooks-hooksts] Run logic at the service layer — fires on every `BaseService` call, whether from an HTTP endpoint or programmatically: ```ts title="src/modules/post/post.hooks.ts" import { BeforeCreateOneHookArgs, AfterCreateOneHookArgs } from "arkos/services"; import { Prisma } from "@prisma/client"; export const beforeCreateOne = [ async ({ data }: BeforeCreateOneHookArgs) => { if (!data.slug && data.title) { data.slug = data.title.toLowerCase().replace(/\s+/g, "-"); } }, ]; export const afterCreateOne = [ async ({ result }: AfterCreateOneHookArgs) => { console.log("Post created via service:", result.id); }, ]; ``` See [Service Hooks](/docs/core-concepts/components/service-hooks) for the full list of available hooks and how they differ from interceptors. ### Prisma Query Options (`*.query.ts`) [#prisma-query-options-queryts] Define default query parameters that Arkos applies automatically to auto-generated endpoints: ```ts title="src/modules/post/post.query.ts" import { Prisma } from "@prisma/client"; import { PrismaQueryOptions } from "arkos/prisma"; const postPrismaQueryOptions: PrismaQueryOptions = { 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 [#validation] **Zod schemas** (`schemas/*.schema.ts`): ```ts title="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`): ```ts title="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 [#packagejson-scripts] `"type": "module"` is only required for JavaScript projects. Remove it if you are using TypeScript. ```json title="package.json" { "type": "module", "scripts": { "dev": "arkos dev", "build": "arkos build", "start": "arkos start", "arkos": "arkos" }, "prisma": { "schema": "prisma/schema/" } } ``` ## Naming Conventions [#naming-conventions] | Convention | Rule | | ------------------ | --------------------------------------------------------------------------- | | Module directories | kebab-case matching Prisma model name — `user-profile`, `order-item` | | File naming | `model-name.file-type.ts` — `post.controller.ts`, `post.service.ts` | | Controller export | Always a class singleton — `export default new PostController(postService)` | | Service export | Always a class singleton — `export default new PostService("post")` | | Interceptors | Always arrays — `export const beforeCreateOne = [...]` | | Service hooks | Always arrays — `export const beforeCreateOne = [...]` | # Email Service Arkos provides an `EmailService` class built on top of [nodemailer](https://www.npmjs.com/package/nodemailer), giving you a simple unified API for sending emails while keeping full access to nodemailer's configuration when you need it. ## Configuration [#configuration] The recommended approach is to set email credentials via environment variables: ```text title=".env" EMAIL_HOST=smtp.gmail.com EMAIL_PORT=465 EMAIL_SECURE=true EMAIL_USER=you@example.com EMAIL_PASSWORD=your-smtp-password EMAIL_NAME=Your App ``` Arkos picks these up automatically. If you prefer to be explicit, wire them into your config: ```ts title="arkos.config.ts" import { defineConfig } from "arkos"; export default defineConfig({ email: { host: process.env.EMAIL_HOST, port: Number(process.env.EMAIL_PORT), secure: process.env.EMAIL_SECURE === "true", auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASSWORD, }, name: process.env.EMAIL_NAME, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { email: { host: process.env.EMAIL_HOST, port: Number(process.env.EMAIL_PORT), secure: process.env.EMAIL_SECURE === "true", auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASSWORD, }, name: process.env.EMAIL_NAME, }, }; export default arkosConfig; ``` ```ts title="src/app.ts" import arkos from "arkos"; arkos.init({ email: { host: process.env.EMAIL_HOST, port: Number(process.env.EMAIL_PORT), secure: process.env.EMAIL_SECURE === "true", auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASSWORD, }, name: process.env.EMAIL_NAME, }, }); ``` | Option | Env Variable | Default | Description | | ----------- | ---------------- | ------- | ----------------------------------------------- | | `host` | `EMAIL_HOST` | — | SMTP server hostname — required | | `port` | `EMAIL_PORT` | `465` | SMTP port | | `secure` | `EMAIL_SECURE` | `true` | Use TLS/SSL — set to `false` for port `587` | | `auth.user` | `EMAIL_USER` | — | SMTP username — required | | `auth.pass` | `EMAIL_PASSWORD` | — | SMTP password — required | | `name` | `EMAIL_NAME` | — | Display name shown alongside the sender address | ## Basic Usage [#basic-usage] Import the default `emailService` instance and call `send()`: ```ts title="src/modules/auth/auth.interceptors.ts" import { emailService } from "arkos/services"; import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; export const afterSignup = [ async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const user = res.locals.data.data; try { const result = await emailService.send({ to: user.email, subject: "Welcome to Our Platform", html: "

Welcome!

Thank you for registering.

", }); console.log(`Email sent successfully. Message ID: ${result.messageId}`); } catch (error) { console.error("Failed to send welcome email:", error); } next(); }, ]; ``` ### Email Options [#email-options] ```ts type EmailOptions = { from?: string; // Overrides the default sender to: string | string[]; // Single recipient or array of recipients subject: string; text?: string; // Plain text version — auto-generated from html if omitted html: string; }; ``` `send()` returns `Promise<{ success: boolean; messageId?: string }>`. ## Advanced Usage [#advanced-usage] ### Custom SMTP Per Send [#custom-smtp-per-send] Send a one-off email through a different SMTP server without changing the default configuration: ```ts await emailService.send( { to: "client@example.com", subject: "Your Invoice", html: "

Please find your invoice attached.

", }, { host: "smtp.yourcompany.com", port: 587, secure: false, auth: { user: "billing@yourcompany.com", pass: "billingPassword", }, } ); ``` ### Multiple Instances [#multiple-instances] If your app needs to send from multiple addresses or SMTP providers, create additional instances: ```ts import { EmailService } from "arkos/services"; const marketingEmailService = new EmailService({ host: "smtp.marketing-provider.com", port: 587, secure: false, auth: { user: "marketing@yourcompany.com", pass: "marketingPassword", }, }); await marketingEmailService.send({ to: "prospects@example.com", subject: "Special Offer", html: "

Limited Time Offer!

", }); ``` ### Updating Configuration [#updating-configuration] Switch an existing instance to a different SMTP account at runtime: ```ts emailService.updateConfig({ host: "smtp.newprovider.com", port: 587, secure: false, auth: { user: "new@example.com", pass: "newPassword", }, }); ``` ## Common Use Cases [#common-use-cases] ### Welcome Email After Signup [#welcome-email-after-signup] ```ts title="src/modules/auth/auth.interceptors.ts" import { emailService } from "arkos/services"; import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; export const afterSignup = [ async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const user = res.locals.data.data; try { await emailService.send({ to: user.email, subject: "Welcome to Our Platform!", html: `

Welcome, ${user.name}!

Thank you for joining our platform.

Get started by completing your profile.

`, }); } catch (error) { console.error("Failed to send welcome email:", error); } next(); }, ]; ``` ### Password Reset Email [#password-reset-email] ```ts title="src/modules/auth/auth.controller.ts" import { emailService } from "arkos/services"; import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; class AuthController { requestPasswordReset = async ( req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { const { email } = req.body; const resetToken = generateResetToken(); await saveResetToken(email, resetToken); await emailService.send({ to: email, subject: "Password Reset Request", html: `

Password Reset

Click the link below to reset your password:

Reset Password

This link will expire in 1 hour.

`, }); res.json({ message: "Password reset email sent" }); }; } export default new AuthController(); ``` ### Order Confirmation Email [#order-confirmation-email] ```ts title="src/modules/order/order.hooks.ts" import { AfterCreateOneHookArgs } from "arkos/services"; import { Prisma } from "@prisma/client"; import { emailService } from "arkos/services"; export const afterCreateOne = [ async ({ result }: AfterCreateOneHookArgs) => { try { await emailService.send({ to: result.customerEmail, subject: `Order Confirmation #${result.id}`, html: `

Thank You for Your Order!

Order #${result.id} has been confirmed.

Order Details:

    ${result.items .map((item) => `
  • ${item.name} - $${item.price}
  • `) .join("")}

Total: $${result.total}

`, }); } catch (error) { console.error("Failed to send order confirmation:", error); } }, ]; ``` ## Best Practices [#best-practices] Always wrap `emailService.send()` in a try-catch. Email failures should not break your application flow — a failed welcome email is not a reason to return a 500 to the user. For the same reason, fire non-critical emails without awaiting them when possible: ```ts emailService.send({ to: user.email, subject: "...", html: "..." }).catch(console.error); ``` For bulk or queued sending, push jobs to a queue rather than sending inline: ```ts await emailQueue.add("welcome-email", { to: req.body.email, name: req.body.name }); ``` Never hardcode SMTP credentials. Always use environment variables. For testing, use a service like [Ethereal Email](https://ethereal.email) to capture outgoing emails without actually delivering them. ## Diving Deeper [#diving-deeper] For the full `EmailService` class API reference see [Email Service](/docs/reference/email-service). # Custom Handler > Available since v1.6.0-beta Arkos registers its global error handler automatically after `app.build()`. You can add your own error middleware right after that call — useful for sending errors to external monitoring services, custom logging sinks, or alerts. ## Adding a Custom Handler [#adding-a-custom-handler] ```ts title="src/app.ts" import arkos from "arkos"; import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; const app = arkos(); app.use(myRouter); await app.build(); // Register your custom error handler here — after build, before listen app.use( ( err: any, req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { // Arkos has already sent the response — use this for side effects only console.error("[CustomHandler]", err.message); next(err); } ); app.listen(); ``` Your custom error handler **must** be registered after `await app.build()`. Registering it before `build()` places it before Arkos's error handler in the middleware chain — it will never receive errors from built-in routes since Arkos's handler runs first and sends the response without calling `next(err)`. Arkos's global error handler sends the response and does not call `next(err)`. Your custom handler runs after it purely for side effects — do not call `res.json()` or `res.send()` inside it. ## Use Cases [#use-cases] ### External Monitoring (Sentry) [#external-monitoring-sentry] ```ts title="src/app.ts" import * as Sentry from "@sentry/node"; await app.build(); app.use( ( err: any, req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { if (!err.isOperational) { // Only report unexpected errors — not 404s, 403s, validation failures Sentry.captureException(err); } next(err); } ); app.listen(); ``` ### Slack Alerts [#slack-alerts] ```ts title="src/app.ts" await app.build(); app.use( async ( err: any, req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { if (err.statusCode >= 500) { await slackClient.postMessage({ channel: "#alerts", text: `[${req.method} ${req.path}] ${err.message}`, }); } next(err); } ); app.listen(); ``` ### Custom Logging [#custom-logging] ```ts title="src/app.ts" await app.build(); app.use( ( err: any, req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { logger.error({ message: err.message, code: err.code, statusCode: err.statusCode, path: req.path, method: req.method, userId: req.user?.id, }); next(err); } ); app.listen(); ``` Always call `next(err)` at the end of your custom handler to keep the Express error chain intact. # Authentication Arkos maps authentication and JWT failures to consistent error responses automatically. These errors fire during the `authenticate` and `authorize` middleware pipeline — you never need to handle them yourself unless you want to add custom behavior via [authentication hooks](/docs/core-concepts/authentication/hooks). ## Token Errors [#token-errors] These fire when Arkos processes the `Authorization` header or `arkos_access_token` cookie: | Scenario | Message | Status | Code | | --------------------------------------- | -------------------------------------------------------- | ------ | -------------------- | | Token is malformed or tampered | `Invalid token. Please log in again!` | `401` | `InvalidToken` | | Token has expired | `Your token has expired, Please log again!` | `401` | `ExpiredToken` | | No token present on a protected route | `You are not logged in! Please log in to get access.` | `401` | `LoginRequired` | | Token valid but user no longer exists | `The user belonging to this token does no longer exists` | `401` | `UserNoLongerExists` | | Password changed after token was issued | `User recently changed password! Please log in again.` | `401` | `PasswordChanged` | ## Login Errors [#login-errors] These fire inside the built-in login endpoint: | Scenario | Message | Status | Code | | ---------------------------------- | ------------------------------------------ | ------ | ------------------------- | | Missing username or password field | `Please provide both {field} and password` | `400` | `MissingCredentialFields` | | Wrong credentials | `Incorrect {field} or password` | `401` | `IncorrectCredentials` | ## Authorization Errors [#authorization-errors] This fires when an authenticated user lacks the required role or permission for a route: | Scenario | Message | Status | Code | | ------------------------ | --------------------------------------------------- | ------ | ---------------------- | | Insufficient permissions | `You do not have permission to perform this action` | `403` | `NotEnoughPermissions` | ## Password Update Errors [#password-update-errors] These fire inside the built-in update-password endpoint: | Scenario | Message | Status | Code | | ------------------------------------------ | ---------------------------------------------- | ------ | --------------------------- | | Missing `currentPassword` or `newPassword` | `currentPassword and newPassword are required` | `400` | `SameCurrentAndNewPassword` | | `currentPassword` is wrong | `Current password is incorrect` | `400` | `IncorrentCurrentPassword` | ## Customizing Authentication Error Behavior [#customizing-authentication-error-behavior] If you need to run custom logic when authentication or authorization fails — logging, suppressing errors for guest access, integrating a third-party auth provider — use [authentication hooks](/docs/core-concepts/authentication/hooks). Hooks let you tap into the `authenticate` and `authorize` pipeline without replacing the built-in behavior. # Prisma Arkos intercepts Prisma client errors and maps them to clean HTTP responses automatically. You never need to catch Prisma errors manually — throw them and the global error handler does the rest. ## Connection and Infrastructure Errors [#connection-and-infrastructure-errors] These fire when Arkos cannot reach or initialize the database: | Prisma Code | Message | Status | Code | | ----------- | ----------------------------------------------------------------------------------------------------- | ------ | --------- | | `P1000` | `Authentication failed against the database server. Please check your credentials.` | `401` | `Unknown` | | `P1001` | `The database server is not reachable. Verify your connection string or ensure the server is online.` | `503` | `Unknown` | | `P1002` | `Connection to the database timed out. Please check server performance or network connectivity.` | `504` | `Unknown` | | `P1003` | `The specified database does not exist on the server.` | `404` | `Unknown` | ## Data Errors [#data-errors] These fire during query and mutation operations: | Prisma Code | Message | Status | Code | | ----------- | --------------------------------------------------------------------------------------------------- | -------------- | ------------------------------------------ | | `P2000` | `The value for the field "{field}" is too large. Please provide a smaller value.` | `400` | `Unknown` | | `P2001` | `No record found for the given query. Ensure the query parameters are correct.` | `404` | `Unknown` | | `P2002` | `Duplicate value detected for the unique field(s): {field}. Please use a different value.` | `409` | `{Model}{Field}UniqueConstraint` | | `P2003` | `Foreign key constraint violation. Ensure that the referenced record exists.` | `400` | `Unknown` | | `P2004` | `A database constraint "{constraint}" failed. Please review your input data.` | `400` | `Unknown` | | `P2025` | Cause from Prisma meta, or `Operation could not be completed as some required record was not found` | `400` or `404` | `InlineRecordNotFound` or `RecordNotFound` | ## Migration Errors [#migration-errors] These fire during schema migration operations: | Prisma Code | Message | Status | Code | | ----------- | ----------------------------------------------------------------------------------- | ------ | --------- | | `P3000` | `Failed to create the database schema. Verify the schema definition and try again.` | `500` | `Unknown` | | `P3001` | `The migration "{name}" has already been applied to the database.` | `409` | `Unknown` | | `P3002` | `The migration script "{name}" failed. Review the script and resolve any issues.` | `500` | `Unknown` | | `P3003` | `Version mismatch: The database schema and migration versions are inconsistent.` | `400` | `Unknown` | ## Validation Errors [#validation-errors] | Error Type | Message | Status | | --------------------------------- | ---------------------------------------- | ------ | | `PrismaClientValidationError` | Last line of Prisma's validation message | `400` | | `PrismaClientInitializationError` | `Service temporarily unavailable` | `503` | For the full list of Prisma error codes and what triggers them, see the [Prisma error reference](https://www.prisma.io/docs/orm/reference/error-reference). # Validation When a request fails validation, Arkos automatically formats the error into a consistent response. The `code` field identifies which part of the request failed — `body`, `query`, or `params`. The `message` field contains the first validation error in human-readable form. The full list of errors is available in `meta`. ## Response Shape [#response-shape] ```json { "status": "error", "code": "InvalidRequestBody", "message": "'email' must be a valid email address", "meta": { "errors": [ { "message": "'email' must be a valid email address", "code": "EmailIsEmailConstraint" }, { "message": "'name' must be at least 2 characters", "code": "NameMinLengthConstraint" } ] } } ``` The `code` field will be one of: | Code | Trigger | | ---------------------- | ------------------------------ | | `InvalidRequestBody` | `req.body` failed validation | | `InvalidRequestQuery` | `req.query` failed validation | | `InvalidRequestParams` | `req.params` failed validation | ## Nested and Array Fields [#nested-and-array-fields] Arkos includes the full field path in error messages so you always know exactly what failed: ```json { "code": "InvalidRequestBody", "message": "'user.profile.bio' must be at least 10 characters", "meta": { "errors": [...] } } ``` ```json { "code": "InvalidRequestBody", "message": "'tags[0].name' must be a string", "meta": { "errors": [...] } } ``` ## Unknown Fields [#unknown-fields] By default Arkos passes blocks unknown fields. You can customize this behavior by using `forbidNonWhitelisted` in your `validationOptions`: ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ validation: { resolver: "zod", validationOptions: { forbidNonWhitelisted: true, }, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { validation: { resolver: "zod", validationOptions: { forbidNonWhitelisted: true, }, }, }; export default arkosConfig; ``` ```ts title="src/app.ts" import arkos from "arkos"; arkos.init({ validation: { resolver: "class-validator", validationOptions: { forbidNonWhitelisted: true, }, }, }); ``` When an unrecognized field is sent: ```json { "status": "error", "code": "InvalidRequestBody", "message": "Unrecognized key(s) in object: 'isAdmin'", "meta": { "errors": [...] } } ``` Enabling `forbidNonWhitelisted` is recommended. It prevents mass assignment vulnerabilities where unexpected fields slip through into your database operations. ## Strict Mode [#strict-mode] Strict mode requires every route to explicitly declare its validation intent at startup — no route can silently pass unvalidated input. See [Validation — Setup](/docs/guides/validation/setup) for the full strict mode rules and behavior table. ## Blocking Input Entirely [#blocking-input-entirely] To prohibit a specific input type on a route entirely — for example, rejecting any query string — pass `null` to that key in `validation`: ```ts router.get( { path: "/api/reports/:id", validation: { body: null, // not allowed — returns 400 if sent }, }, reportController.getOne ); ``` See [Validation — Setup](/docs/guides/validation/setup) for the full behavior table. # Overview Arkos ships a global error handler that sits at the end of the Express middleware chain and processes every error thrown or passed via `next(err)` across your application. It normalizes errors into a consistent response shape, maps known error types (Prisma, JWT, validation) to human-readable messages, and adjusts the level of detail it exposes based on the current environment. You never register this handler yourself — Arkos wires it in automatically. ## How It Works [#how-it-works] When any middleware, interceptor, service hook, or controller throws an error or calls `next(err)`, Express forwards it to the global error handler. From there: 1. The error is matched against known types — Prisma errors, JWT errors, validation errors — and mapped to a friendly message and status code. 2. The response is shaped based on the environment. 3. The response is sent to the client. Because `ArkosRouter` wraps all handlers with `catchAsync` automatically, you never need to wrap your own handlers — thrown errors are always caught and forwarded correctly. ## Environments [#environments] Arkos determines the environment through its own CLI commands, not through `NODE_ENV`: * **Development** — run with `npx arkos dev`. Full error details are exposed including stack traces, useful for debugging. * **Production** — run with `npx arkos start`. Responses are sanitized. Stack traces are hidden. Non-operational errors return a generic message. ### Development Response [#development-response] ```json { "message": "User not found with id: 123", "code": "NotFound", "statusCode": 404, "status": "fail", "isOperational": true, "meta": {}, "stack": [ "AppError: User not found with id: 123", " at findOne (/src/modules/base/base.controller.ts:25:11)" ] } ``` ### Production Response [#production-response] Operational errors (expected failures like 404, 403, 400) include the message: ```json { "status": "fail", "message": "User not found with id: 123", "code": "NotFound", "meta": {} } ``` Non-operational errors (programming errors, unexpected failures) return a generic message to avoid leaking internals: ```json { "status": "error", "message": "Internal server error, please try again later.", "code": "Unknown", "meta": {} } ``` `NODE_ENV` does not control Arkos's error response behavior. Use `npx arkos dev` for development and `npx arkos start` for production. ## What's Next [#whats-next] * [Usage](/docs/guides/error-handling/usage) — throwing errors correctly with `AppError` * [Error Messages](/docs/guides/error-handling/error-messages/validation) — how Arkos formats validation, authentication, and Prisma errors * [Custom Handler](/docs/guides/error-handling/custom-handler) — adding your own error handler after Arkos's # Usage The foundation of error handling in Arkos is `AppError` — a structured error class that plugs into the global error handler. Every error you throw in a controller, interceptor, or service should be an `AppError`. Never send error responses manually with `res.json()` or `res.status().json()`. ## AppError [#apperror] ```ts import { AppError } from "arkos/error-handler"; throw new AppError(message, statusCode, code?, meta?); ``` | Parameter | Type | Required | Description | | ------------ | -------- | -------- | -------------------------------------------------- | | `message` | `string` | Yes | Human-readable error message shown to the client | | `statusCode` | `number` | Yes | HTTP status code (400, 401, 403, 404, 409, 500...) | | `code` | `string` | No | Machine-readable code for client-side handling | | `meta` | `object` | No | Additional context — avoid sensitive data | ```ts throw new AppError( "Email already registered", 409, "EmailAlreadyExists", { email: req.body.email } ); ``` Response: ```json { "status": "fail", "message": "Email already registered", "code": "EmailAlreadyExists", "meta": { "email": "user@example.com" } } ``` ## Always Throw, Never Respond [#always-throw-never-respond] The single most important practice: throw `AppError` instead of writing custom error responses. Manual responses bypass the global error handler, break response consistency, skip `onError` interceptors, and won't be included in future OpenAPI error documentation. **Don't do this:** ```ts import { BaseController } from "arkos/controllers"; import { ArkosRequest, ArkosResponse } from "arkos"; import { AppError } from "arkos/error-handler"; import userService from "./user.service"; class UserController extends BaseController { getUser = async (req: ArkosRequest, res: ArkosResponse) => { const user = await userService.findOne({ id: req.params.id }); if (!user) { return res.status(404).json({ error: "User not found" }); // ❌ } res.json({ data: user }); }; } export default new UserController(); ``` **Do this:** ```ts import { BaseController } from "arkos/controllers"; import { ArkosRequest, ArkosResponse } from "arkos"; import { AppError } from "arkos/error-handler"; import userService from "./user.service"; class UserController extends BaseController { getUser = async (req: ArkosRequest, res: ArkosResponse) => { const user = await userService.findOne({ id: req.params.id }); if (!user) { throw new AppError(`User not found with id: ${req.params.id}`, 404, "NotFound"); // ✅ } res.json({ data: user }); }; } export default new UserController(userService); ``` Because `ArkosRouter` wraps all handlers with `catchAsync` automatically, you never need to wrap handlers or call `next(err)` manually for thrown errors — just throw. ## In-Route Error Handlers [#in-route-error-handlers] For cases where you need to catch an error locally — for instance to add context before re-throwing, or to handle a third-party call that throws its own error type — use a standard Express error handler (4-param middleware) registered on the route itself. Always rethrow as `AppError` so the global handler receives a normalized error: ```ts title="src/modules/payment/payment.router.ts" import { ArkosRouter } from "arkos"; import { AppError } from "arkos/error-handler"; import paymentController from "./payment.controller"; import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; const paymentRouter = ArkosRouter(); paymentRouter.post( { path: "/api/payments" }, paymentController.charge, // in-route error handler — catches errors from paymentController.charge (err: any, req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { // Normalize third-party Stripe errors into AppError before forwarding if (err?.type === "StripeCardError") { return next(new AppError(err.message, 402, "PaymentFailed", { declineCode: err.decline_code })); } next(err); // anything else goes straight to the global handler } ); export default paymentRouter; ``` In-route error handlers must have exactly 4 parameters `(err, req, res, next)` — Express uses the parameter count to identify them as error handlers. See [Express error handling docs](https://expressjs.com/en/guide/error-handling.html) for more. ## In-Route Error Handling For Built-in Routes [#in-route-error-handling-for-built-in-routes] ```ts title="src/modules/post/post.interceptors.ts" // For built-in routes, use interceptor error handlers instead of in-route handlers export const onCreateOneError = [ async (err: any, req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { if (req.uploadedImageUrl) await deleteFromS3(req.uploadedImageUrl); next(err); }, ]; ``` Read more at [Interceptors — Handling Errors](/docs/core-concepts/components/interceptors#onerror-interceptors) ## In-Line Error Handling For Service Layer [#in-line-error-handling-for-service-layer] ```ts title="src/modules/post/post.hooks.ts" // For service-level errors export const onCreateOneError = [ async ({ error, data }) => { console.error("Service error:", error.message); }, ]; ``` Read more at [Service Hooks — Handling Errors](/docs/core-concepts/components/service-hooks#onerror-service-hooks) ## Common Status Codes [#common-status-codes] | Code | When to use | | ----- | ------------------------------------------------ | | `400` | Invalid input, bad request | | `401` | Not authenticated | | `403` | Authenticated but not authorized | | `404` | Resource not found | | `409` | Conflict — duplicate entry, constraint violation | | `500` | Unexpected server error | ## Error Hooks [#error-hooks] If you need to run cleanup or rollback logic when an operation fails — uploaded files, reserved inventory, database transactions — use `onError` interceptors or service hook error handlers rather than try/catch blocks in your controllers: * `onXxxError` in `*.interceptors.ts` — runs at the HTTP layer. See [Interceptors — Handling Errors](/docs/core-concepts/components/interceptors#handling-errors-in-interceptors). * `onXxxError` in `*.hooks.ts` — runs at the service layer for all calls. See [Service Hooks — Handling Errors](/docs/core-concepts/components/service-hooks#handling-errors-in-service-hooks). # Router Upload Router uploads let you attach file handling directly to a route so that the file path is automatically injected into `req.body` before your handler runs — no separate upload step required. This works on both custom routes via [ArkosRouter](/docs/core-concepts/components/routers) and built-in routes via [RouteHook](/docs/core-concepts/components/route-hooks). If you need to upload files independently of any route or model operation, see [File Upload Routes](/docs/guides/file-handling/file-uploads/routes) for the standalone approach instead. ## Upload types [#upload-types] Arkos supports three upload strategies, all configured through the `experimental.uploads` key. ### single [#single] One file per request. The file path is attached to `req.body[field]` as a string. ```ts title="src/routers/reports.router.ts" import { ArkosRouter } from "arkos"; import reportsController from "../controllers/reports.controller"; const reportsRouter = ArkosRouter(); reportsRouter.post( { path: "/api/reports/upload", authentication: true, experimental: { uploads: { type: "single", field: "reportFile", uploadDir: "reports", maxSize: 50 * 1024 * 1024, allowedFileTypes: [".xlsx", ".csv", ".pdf"], deleteOnError: true, }, }, }, reportsController.processUploadedReport ); export default reportsRouter; ``` `RouteHook` is the new name for `export const config: RouterConfig`. Existing code using the old name still works but will log a deprecation warning. See the [Route Hook guide](/docs/core-concepts/components/route-hooks) for details. ```ts title="src/modules/user/user.router.ts" import { ArkosRouter, RouteHook } from "arkos"; export const hook: RouteHook = { createOne: { experimental: { uploads: { type: "single", field: "profilePhoto", uploadDir: "user-profiles", maxSize: 5 * 1024 * 1024, allowedFileTypes: [".jpg", ".jpeg", ".png", ".webp"], deleteOnError: true, }, }, }, updateOne: { experimental: { uploads: { type: "single", field: "profilePhoto", uploadDir: "user-profiles", required: false, }, }, }, }; const userRouter = ArkosRouter(); export default userRouter; ``` ### array [#array] Multiple files under one field. The paths are attached to `req.body[field]` as a `string[]`. ```ts title="src/routers/gallery.router.ts" import { ArkosRouter } from "arkos"; import galleryController from "../controllers/gallery.controller"; const galleryRouter = ArkosRouter(); galleryRouter.post( { path: "/api/gallery", authentication: true, experimental: { uploads: { type: "array", field: "photos", maxCount: 12, uploadDir: "gallery", deleteOnError: true, }, }, }, galleryController.createAlbum ); export default galleryRouter; ``` `RouteHook` is the new name for `export const config: RouterConfig`. Existing code using the old name still works but will log a deprecation warning. See the [Route Hook guide](/docs/core-concepts/components/route-hooks) for details. ```ts title="src/modules/product/product.router.ts" import { ArkosRouter, RouteHook } from "arkos"; export const hook: RouteHook = { createOne: { experimental: { uploads: { type: "array", field: "images", maxCount: 8, uploadDir: "product-images", deleteOnError: true, }, }, }, }; const productRouter = ArkosRouter(); export default productRouter; ``` ### fields [#fields] Multiple named fields, each with its own `maxCount`. Use this when a single request carries files of fundamentally different kinds. ```ts title="src/routers/listings.router.ts" import { ArkosRouter } from "arkos"; import listingsController from "../controllers/listings.controller"; const listingsRouter = ArkosRouter(); listingsRouter.post( { path: "/api/listings", authentication: true, experimental: { uploads: { type: "fields", fields: [ { name: "thumbnail", maxCount: 1 }, { name: "gallery", maxCount: 6 }, { name: "floorPlan", maxCount: 1 }, ], uploadDir: "listings", deleteOnError: true, }, }, }, listingsController.createListing ); export default listingsRouter; ``` ```ts title="src/modules/product/product.router.ts" import { ArkosRouter, RouteHook } from "arkos"; export const hook: RouteHook = { createOne: { experimental: { uploads: { type: "fields", fields: [ { name: "thumbnail", maxCount: 1 }, { name: "gallery", maxCount: 6 }, { name: "manual", maxCount: 1 }, ], uploadDir: "products", deleteOnError: true, }, }, }, }; const productRouter = ArkosRouter(); export default productRouter; ``` `RouteHook` is the new name for `export const config: RouterConfig`. Existing code using the old name still works but will log a deprecation warning. See the [Route Hook guide](/docs/core-concepts/components/route-hooks) for details. ## Nested fields [#nested-fields] Bracket notation maps uploaded files into nested `req.body` shapes. This works the same way in both `ArkosRouter` and `RouteHook`. ```ts experimental: { uploads: { type: "single", field: "profile[photo]", uploadDir: "user-profiles", }, }, ``` The file path lands at `req.body.profile.photo`. ## `experimental.uploads` reference [#experimentaluploads-reference] | Property | Type | Required | Default | Description | | ------------------ | ------------------------------------------- | ---------------------- | ------------------ | ------------------------------------------------ | | `type` | `"single" \| "array" \| "fields"` | ✓ | — | Upload strategy | | `field` | `string` | For `single` / `array` | — | FormData field name. Supports bracket notation. | | `fields` | `Array<{ name: string; maxCount: number }>` | For `fields` | — | Per-field config | | `required` | `boolean` | — | `true` | Mark file as optional with `false` | | `uploadDir` | `string` | — | Auto by MIME | Subdirectory inside `baseUploadDir` | | `maxSize` | `number` | — | From global config | Per-file size limit in bytes | | `maxCount` | `number` | For `array` | — | Max number of files | | `allowedFileTypes` | `string[] \| RegExp` | — | From global config | Allowed extensions or pattern | | `attachToBody` | `"pathname" \| "url" \| "file" \| false` | — | `"pathname"` | How the file reference is attached to `req.body` | | `deleteOnError` | `boolean` | — | `true` | Delete uploaded files if the request fails | ## Shape of `attachToBody` [#shape-of-attachtobody] The value written to `req.body[field]` (or `req.body.fields[name]`) depends on `attachToBody`: * `"pathname"` (default) → `string`, e.g. `"/api/uploads/documents/report-123.xlsx"` * `"url"` → `string`, e.g. `"https://yourdomain.com/api/uploads/documents/report-123.xlsx"` * `"file"` → the full Multer file object, extended with `url` and `pathname` aka Arkos file object: ```ts { fieldname: string; originalname: string; encoding: string; mimetype: string; size: number; destination: string; filename: string; path: string; url: string; // added by Arkos pathname: string; // added by Arkos } ``` * `false` → nothing is attached to `req.body`; read the file(s) from `req.file` / `req.files` instead. For `type: "array"`, this value becomes an array of the shape above. For `type: "fields"`, each named field gets a single object (one file) or an array (multiple files). ## Related [#related] * [File Upload Setup](/docs/guides/file-handling/file-uploads/setup) — global configuration, `baseUploadDir`, `baseRoute`, restrictions * [File Upload Routes](/docs/guides/file-handling/file-uploads/routes) — standalone endpoints and request format * [Interceptors](/docs/core-concepts/components/interceptors#file-upload-interceptors) — before/after hooks for file upload operations # Routes Arkos automatically exposes dedicated routes for standalone file operations. These work independently of your Prisma models and are available out of the box once file uploads are configured. | Method | Endpoint | Description | Operation | | ------ | ---------------------------------- | ------------------------ | ------------ | | GET | `/api/uploads/:fileType/:fileName` | Serve/retrieve a file | `findFile` | | POST | `/api/uploads/:fileType` | Upload files | `uploadFile` | | PATCH | `/api/uploads/:fileType/:fileName` | Replace an existing file | `updateFile` | | DELETE | `/api/uploads/:fileType/:fileName` | Delete a file | `deleteFile` | `:fileType` accepts: `images`, `videos`, `documents`, or `files`. `/api/uploads` is the default base route and can be customized in your config. See [File Upload Setup](/docs/guides/file-handling/file-uploads/setup) for details. ## Two approaches to file uploads [#two-approaches-to-file-uploads] These standalone routes are one way to handle file uploads. Arkos also supports declaring uploads directly on your routes via `experimental.uploads` — a single API call that handles both the file and the record together. **Use standalone routes when:** * You need to upload files independently of any model operation * You're building a file management system * You need to upload files before deciding where to use them **Use router uploads when:** * Files are tied to a model (user avatars, post images, product galleries) * You want a single API call that handles data and files together * You want automatic cleanup on error via `deleteOnError` See [Router Upload](/docs/guides/file-handling/file-uploads/router-upload) for the full router upload guide. ## Configuring file upload routes [#configuring-file-upload-routes] Use the `hook` named export in `src/modules/file-upload/file-upload.router.ts` to configure any file upload endpoint. Each key maps to the operation name from the table above. `RouteHook` is the new name for `export const config: RouterConfig`. Existing code using the old name still works but will log a deprecation warning. See the [Route Hook guide](/docs/core-concepts/components/route-hooks) for details. ```ts title="src/modules/file-upload/file-upload.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import uploadPolicy from "@/src/modules/file-upload/file-upload.policy"; export const hook: RouteHook = { findFile: { authentication: false }, uploadFile: { authentication: uploadPolicy.Upload }, updateFile: { authentication: true }, deleteFile: { disabled: true }, }; const router = ArkosRouter(); export default router; ``` See the full configuration object reference at [ArkosRouter](/docs/reference/arkos-router). ## Intercepting file upload requests [#intercepting-file-upload-requests] Every file upload endpoint can be intercepted — run logic before or after any operation. For example, to check if a file is still referenced before deleting it: ```ts title="src/modules/file-upload/file-upload.interceptors.ts" import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; export const beforeDeleteFile = [ async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const { fileName } = req.params; const isReferenced = await checkFileReferences(fileName); if (isReferenced) { return res.status(400).json({ code: "FileStillReferenced", message: "Cannot delete file: still referenced in database", }); } next(); }, ]; ``` Available hooks: `beforeUploadFile`, `afterUploadFile`, `beforeUpdateFile`, `afterUpdateFile`, `beforeDeleteFile`, `afterDeleteFile`, `beforeFindFile`. See [Interceptors](/docs/core-concepts/components/interceptors#file-upload-interceptors) for full details. ## Sending requests [#sending-requests] ### Retrieve a file [#retrieve-a-file] ```http GET /api/uploads/images/1234567890-photo.jpg ``` Response: The file is served directly as a static asset. ### Upload a file [#upload-a-file] When uploading, the FormData field name must match the `:fileType` in the URL. ```http POST /api/uploads/images Authorization: Bearer YOUR_API_TOKEN Content-Type: multipart/form-data ``` ```ts const formData = new FormData(); formData.append("images", imageFile); // field name matches fileType const response = await fetch("http://localhost:8000/api/uploads/images", { method: "POST", body: formData, }); ``` Response: ```json { "success": true, "message": "File uploaded successfully", "urls": ["http://localhost:8000/uploads/images/1234567890-photo.jpg"] } ``` **Image processing via query parameters:** ```http POST /api/uploads/images?resizeTo=800&format=webp ``` | Parameter | Description | | --------------- | ------------------------------------ | | `?width=500` | Resize to width (may distort ratio) | | `?height=300` | Resize to height (may distort ratio) | | `?resizeTo=800` | Resize to fit within px, keeps ratio | | `?format=webp` | Convert to format | ### Replace a file [#replace-a-file] Automatically deletes the old file before uploading the new one. ```http PATCH /api/uploads/images/old-image.jpg Content-Type: multipart/form-data Authorization: Bearer YOUR_API_TOKEN ``` ```ts const formData = new FormData(); formData.append("images", newImageFile); const response = await fetch( "http://localhost:8000/api/uploads/images/old-image.jpg", { method: "PATCH", body: formData } ); ``` Response: ```json { "success": true, "message": "File replaced successfully", "urls": ["http://localhost:8000/uploads/images/new-image.jpg"] } ``` If the old file doesn't exist, it acts as a regular upload and returns `"File uploaded successfully"` instead. ### Delete a file [#delete-a-file] ```http DELETE /api/uploads/images/1234567890-photo.jpg Authorization: Bearer YOUR_API_TOKEN ``` Response: `204 No Content` ## Common workflow [#common-workflow] Upload a file first, then reference it in a subsequent model request: ```ts // 1. Upload the file const formData = new FormData(); formData.append("images", file); const uploadResponse = await fetch("http://localhost:8000/api/uploads/images", { method: "POST", body: formData, }); const { urls } = await uploadResponse.json(); const fileUrl = urls[0]; // 2. Use the URL in a model update await fetch("http://localhost:8000/api/users/123", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ profilePhoto: fileUrl }), }); ``` ## Default file type support [#default-file-type-support] **Images** — jpeg, jpg, png, gif, webp, svg, bmp, tiff, heic, avif, psd, and more. Defaults: 30 files max, 15 MB per file. **Videos** — mp4, avi, mov, mkv, webm, flv, wmv, and more. Defaults: 10 files max, 5 GB per file. **Documents** — pdf, doc, docx, xls, xlsx, ppt, pptx, csv, txt, epub, md, and more. Defaults: 30 files max, 50 MB per file. **Files** — any other type. Defaults: 10 files max, 5 GB per file. ## Configuration [#configuration] ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ fileUpload: { baseUploadDir: "/uploads", baseRoute: "/api/uploads", expressStatic: { maxAge: "1d", }, restrictions: { images: { maxCount: 10, maxSize: 5 * 1024 * 1024, supportedFilesRegex: /\.(jpg|jpeg|png|webp)$/, }, }, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const config: ArkosConfig = { fileUpload: { baseUploadDir: "/uploads", baseRoute: "/api/uploads", expressStatic: { maxAge: "1d", }, restrictions: { images: { maxCount: 10, maxSize: 5 * 1024 * 1024, supportedFilesRegex: /\.(jpg|jpeg|png|webp)$/, }, }, }, }; export default config; ``` ```ts title="src/app.ts" import arkos from "arkos"; arkos.init({ fileUpload: { baseUploadDir: "/uploads", baseRoute: "/api/uploads", restrictions: { images: { maxCount: 10, maxSize: 5 * 1024 * 1024, supportedFilesRegex: /\.(jpg|jpeg|png|webp)$/, }, }, }, }); ``` ``` ## Related - [Router Upload](/docs/guides/file-handling/file-uploads/router-upload) — attaching uploads to custom and built-in routes - [Interceptors](/docs/core-concepts/components/interceptors#file-upload-interceptors) — before/after hooks for any file operation - [File Upload Services](/docs/reference/file-upload-services) — programmatic file operations inside controllers and services - [Validation with file uploads](/docs/guides/validation/usage#validation-with-file-uploads) — how to combine `validation.body` and `experimental.uploads` in the same request ``` # Setup Arkos supports two approaches to file uploads: **standalone routes** (`/api/uploads/:fileType`) that handle files independently, and **router uploads** that attach files directly to a route in a single request. ```ts // Standalone — upload first, reference the URL later const { urls } = await fetch("/api/uploads/images", { method: "POST", body: formData }).then(r => r.json()); ``` See [File Upload Routes](/docs/guides/file-handling/file-uploads/routes) for the full standalone API. ## Router uploads [#router-uploads] Use `experimental.uploads` on any route definition to handle file uploads inline — the file path is automatically attached to `req.body` before your handler runs. ```ts title="src/routers/reports.router.ts" reportsRouter.post( { path: "/api/reports/upload", experimental: { uploads: { type: "single", field: "reportFile", uploadDir: "reports", deleteOnError: true, }, }, }, reportsController.processUploadedReport ); ``` ```ts title="src/modules/user/user.router.ts" import { ArkosRouter, RouteHook } from "arkos"; export const hook: RouteHook = { createOne: { experimental: { uploads: { type: "single", field: "profilePhoto", uploadDir: "user-profiles", deleteOnError: true, }, }, }, }; const userRouter = ArkosRouter(); export default userRouter; ``` `RouteHook` is the new name for `export const config: RouterConfig`. Existing code using the old name still works but will log a deprecation warning. See the [Route Hook guide](/docs/core-concepts/components/route-hooks) for details. For all upload types (`single`, `array`, `fields`), nested field notation, and `RouteHook` examples, see [Router Upload](/docs/guides/file-handling/file-uploads/router-upload). ## Global configuration [#global-configuration] All fields are optional and deep-merged with defaults. | Key | Type | Description | Default | | ------------------------ | -------- | ---------------------------------------------------------------- | -------------- | | `baseUploadDir` | `string` | Root-relative path where files are saved | `/uploads` | | `baseRoute` | `string` | Base route for standalone upload endpoints | `/api/uploads` | | `expressStatic` | `object` | Options passed to `express.static()` — deep-merged with defaults | See below | | `restrictions.images` | `object` | `maxCount`, `maxSize`, `supportedFilesRegex` for images | - | | `restrictions.videos` | `object` | Same shape, for videos | — | | `restrictions.documents` | `object` | Same shape, for documents | — | | `restrictions.files` | `object` | Same shape, for all other types | — | `expressStatic` defaults: ```ts { maxAge: "1y", etag: true, lastModified: true, dotfiles: "ignore", fallthrough: true, index: false, cacheControl: true, } ``` `baseRoute` and `baseUploadDir` are independent — changing one does not affect the other. ```ts title="arkos.config.ts" import { defineConfig } from "arkos"; export default defineConfig({ fileUpload: { baseUploadDir: "/uploads", baseRoute: "/api/uploads", expressStatic: { maxAge: "1d", }, restrictions: { images: { maxCount: 10, maxSize: 5 * 1024 * 1024, supportedFilesRegex: /\.(jpg|jpeg|png|webp)$/, }, }, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { fileUpload: { baseUploadDir: "/uploads", baseRoute: "/api/uploads", expressStatic: { maxAge: "1d", }, restrictions: { images: { maxCount: 10, maxSize: 5 * 1024 * 1024, supportedFilesRegex: /\.(jpg|jpeg|png|webp)$/, }, }, }, }; export default arkosConfig; ``` ```ts title="src/app.ts" import arkos from "arkos"; arkos.init({ fileUpload: { baseUploadDir: "/uploads", baseRoute: "/api/uploads", restrictions: { images: { maxCount: 10, maxSize: 5 * 1024 * 1024, supportedFilesRegex: /\.(jpg|jpeg|png|webp)$/, }, }, }, }); ``` ## Related [#related] * [Router Upload](/docs/guides/file-handling/file-uploads/router-upload) — attaching uploads to custom and built-in routes * [File Upload Routes](/docs/guides/file-handling/file-uploads/routes) — standalone endpoints, request format, image processing params * [Interceptors](/docs/core-concepts/components/interceptors#file-upload-interceptors) — before/after hooks for any file operation * [Validation with file uploads](/docs/guides/validation/usage#validation-with-file-uploads) — how to combine `validation.body` and `experimental.uploads` in the same request # Static Files > Available from `v1.7.0-beta` Arkos automatically serves files from a `public/` folder in your project root. Any file placed there is accessible directly by its path — no route configuration required. ```http GET /public/logo.svg GET /favicon.ico ``` Static files are **not** prefixed with `globalPrefix`. A file at `public/logo.svg` is served at `/logo.svg`, not `/api/logo.svg`. ## How it works [#how-it-works] On startup, Arkos checks for the configured static folder. If it doesn't exist, it creates it and emits a warning. To disable static file serving entirely, set `staticFiles.enabled = false` in your config. ## Configuration [#configuration] All fields are optional and deep-merged with defaults. | Key | Type | Description | Default | | --------------- | --------- | ---------------------------------------------------------------- | ---------- | | `enabled` | `boolean` | Whether to enable static file serving | `true` | | `folder` | `string` | Folder to serve, relative to project root | `"public"` | | `prefix` | `string` | URL prefix under which files are served | `"/"` | | `expressStatic` | `object` | Options passed to `express.static()` — deep-merged with defaults | See below | `expressStatic` defaults: ```ts { maxAge: "1y", etag: true, lastModified: true, dotfiles: "ignore", fallthrough: true, index: false, cacheControl: true, } ``` ```ts title="arkos.config.ts" import { defineConfig } from "arkos"; export default defineConfig({ staticFiles: { folder: "assets", prefix: "/static", expressStatic: { maxAge: "7d", }, }, }); ``` ## Opting out [#opting-out] ```ts title="arkos.config.ts" import { defineConfig } from "arkos"; export default defineConfig({ staticFiles: { enabled: false, }, }); ``` ## Related [#related] * [File Upload Setup](/docs/guides/file-handling/file-uploads/setup) — handling user-uploaded files # Global Middlewares Arkos ships a set of global middlewares that run on every request before any route handler. They are enabled by default with sensible defaults and can be customized, replaced, or disabled through `arkos.config.ts`. ## Configuration Pattern [#configuration-pattern] Every built-in middleware follows the same three-way pattern under `middlewares.*`: | Value | Behavior | | ------------------------------ | ------------------------------------------------- | | omitted | Runs with Arkos defaults | | options object | Deep-merged into the defaults | | `ArkosRequestHandler` function | Your middleware runs instead — Arkos's is removed | | `false` | Middleware is disabled entirely | Replacing or disabling middlewares like `cors`, `express-json`, or `global-error-handler` can break your application. Do it only if you know what you're replacing them with. ## Middlewares [#middlewares] ### Compression [#compression] Compresses response bodies using the `compression` npm package. **Default:** enabled with no options. ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ middlewares: { compression: { level: 6, threshold: 1024 }, // options // compression: myMiddleware, // replace // compression: false, // disable }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { middlewares: { compression: { level: 6, threshold: 1024 }, // compression: myMiddleware, // compression: false, }, }; export default arkosConfig; ``` See [compression npm package](https://www.npmjs.com/package/compression) for all available options. ### Rate Limit [#rate-limit] Limits the number of requests per IP across all endpoints using `express-rate-limit`. **Default:** ```ts { windowMs: 60 * 1000, limit: 300, standardHeaders: "draft-7", legacyHeaders: false, } ``` ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ middlewares: { rateLimit: { windowMs: 60 * 1000, limit: 100 }, // options — deep-merged // rateLimit: myMiddleware, // replace // rateLimit: false, // disable }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { middlewares: { rateLimit: { windowMs: 60 * 1000, limit: 100 }, }, }; export default arkosConfig; ``` Authentication endpoints have their own separate rate limit configured under `authentication.requestRateLimitOptions`. See [Authentication — Setup](/docs/core-concepts/authentication/setup) for details. See [express-rate-limit npm package](https://www.npmjs.com/package/express-rate-limit) for all available options. ### CORS [#cors] Controls cross-origin resource sharing using the `cors` npm package. **Default:** all origins blocked unless `allowedOrigins` is set. ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ middlewares: { cors: { allowedOrigins: ["https://myapp.com", "https://admin.myapp.com"], // allowedOrigins: "*", // allow all options: { credentials: true }, // customHandler: myCorsDelegate, // replace cors logic only }, // cors: myMiddleware, // replace the entire middleware // cors: false, // disable }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { middlewares: { cors: { allowedOrigins: ["https://myapp.com", "https://admin.myapp.com"], options: { credentials: true }, }, }, }; export default arkosConfig; ``` See [cors npm package](https://www.npmjs.com/package/cors) for all available options. ### JSON Body Parser [#json-body-parser] Parses incoming `application/json` request bodies using `express.json()`. **Default:** enabled with no options. ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ middlewares: { expressJson: { limit: "10mb" }, // options // expressJson: myMiddleware, // replace // expressJson: false, // disable }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { middlewares: { expressJson: { limit: "10mb" }, }, }; export default arkosConfig; ``` ### Cookie Parser [#cookie-parser] Parses `Cookie` headers using the `cookie-parser` npm package. **Default:** enabled with no parameters. ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ middlewares: { cookieParser: ["mySecretSigningKey"], // parameters array // cookieParser: myMiddleware, // replace // cookieParser: false, // disable }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { middlewares: { cookieParser: ["mySecretSigningKey"], }, }; export default arkosConfig; ``` See [cookie-parser npm package](https://www.npmjs.com/package/cookie-parser) for all available options. ### Query Parser [#query-parser] Automatically coerces query string values from strings into their correct types — `null`, `undefined`, `boolean`, and `number`. **Default:** ```ts { parseNull: true, parseUndefined: true, parseBoolean: true, parseNumber: true, } ``` ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ middlewares: { queryParser: { parseNull: true, parseBoolean: true, parseNumber: false }, // options // queryParser: myMiddleware, // replace // queryParser: false, // disable }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { middlewares: { queryParser: { parseNull: true, parseBoolean: true, parseNumber: false }, }, }; export default arkosConfig; ``` `parseNumber` is enabled by default. Any query string field containing only digits will be converted to a number — including fields you may intend to keep as strings. Disable it if that causes issues in your app. ### Request Logger [#request-logger] Logs incoming requests. Useful for debugging and monitoring. **Default:** enabled. ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ middlewares: { requestLogger: myLoggerMiddleware, // replace // requestLogger: false, // disable }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { middlewares: { requestLogger: myLoggerMiddleware, // requestLogger: false, }, }; export default arkosConfig; ``` ### Helmet [#helmet] > Available since v1.6.0-beta Sets security-related HTTP response headers using the `helmet` npm package. **Default:** enabled with no options. ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ middlewares: { helmet: {}, // options // helmet: myMiddleware, // replace // helmet: false, // disable }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; import helmet from "helmet"; const arkosConfig: ArkosConfig = { configureApp: (app) => { app.use(helmet()); }, }; export default arkosConfig; ``` Install helmet first: `npm install helmet` or `pnpm add helmet`. See [helmet npm package](https://www.npmjs.com/package/helmet) for all available options. ### Global Error Handler [#global-error-handler] Arkos registers its global error handler automatically. You can replace it entirely or disable it. ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ middlewares: { errorHandler: myErrorHandler, // replace // errorHandler: false, // disable }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { middlewares: { errorHandler: myErrorHandler, // errorHandler: false, }, }; export default arkosConfig; ``` Replacing the global error handler means you take full responsibility for formatting and sending error responses. See [Error Handling — Overview](/docs/guides/error-handling/overview) for what Arkos's handler does by default. If you want to run additional logic **alongside** Arkos's error handler rather than replacing it, use a [Custom Handler](/docs/guides/error-handling/custom-handler) instead. ## Additional Middlewares [#additional-middlewares] To inject custom middlewares into the global stack: ```ts title="src/app.ts" import arkos from "arkos"; const app = arkos(); app.use(myCustomMiddleware); app.listen(); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { configureApp: (app) => { app.use(myCustomMiddleware); }, }; export default arkosConfig; ``` ## Execution Order [#execution-order] The global middleware stack runs in this order on every request: 1. `compression` 2. `rateLimit` 3. `cors` 4. `expressJson` 5. `cookieParser` 6. `queryParser` 7. `requestLogger` 8. `additional` middlewares 9. Routers (file upload, auth, prisma models, custom) 10. Catch-all 404 11. `errorHandler` # Migrate From Express Under development! # Migrate From Fastify Under development! # Migrate From Hono Under development! # Migrate From NestJS Under development! # Authentication Arkos integrates directly with its authentication system to secure your OpenAPI documentation in production. When enabled, it provides a built-in login UI that respects your `login.allowedUsernames` configuration. ## Configuration [#configuration] ```ts title="arkos.config.ts" import { defineConfig } from "arkos"; export default defineConfig({ authentication: { login: { allowedUsernames: ["email", "staffCode"], // or ["username"], ["email"] }, }, swagger: { endpoint: "/api/docs", authenticate: true, // Require login to access docs (default: true in production, false in development) enableAfterBuild: true, // Keep docs available after build (default: true) }, }); ``` | Option | Default | Description | | ------------------ | -------------------------------------------- | -------------------------------------------------- | | `authenticate` | `true` in production, `false` in development | Require authentication to access docs | | `enableAfterBuild` | `true` | Keep documentation available after `npm run build` | ## Built-in Login UI [#built-in-login-ui] When authentication is enabled, Arkos serves a login page at `/api/docs/auth/login` that: * Automatically generates fields based on your `allowedUsernames` configuration * Supports multiple login field types via a dropdown selector * Matches your Scalar theme colors * Redirects back to documentation after successful login The login page adapts to your auth configuration: * **Single field** — shows one input with the formatted field label (e.g., "Email", "Staff Code") * **Multiple fields** — adds a dropdown to select which field to log in with ## Production Behavior [#production-behavior] After `npm run build`: 1. Documentation endpoint is disabled by default (`enableAfterBuild: false`) 2. If enabled and `authenticate: true`, only superusers can access docs 3. Unauthenticated users are redirected to the built-in login page ## Manual Access Control [#manual-access-control] For custom authentication requirements, override the default behavior: ```ts title="arkos.config.ts" export default defineConfig({ swagger: { authenticate: false, // Disable built-in auth enableAfterBuild: true, // Keep docs available after build }, }); ``` Then implement your own middleware to protect the `/api/docs` route. ## Next Steps [#next-steps] * [File Uploads Integration](/docs/guides/open-api-documentation/integrations/file-uploads) — Automatic multipart/form-data documentation * [Prisma Integration](/docs/guides/open-api-documentation/integrations/prisma) — How query options shape generated schemas * [Documenting Routes](/docs/guides/open-api-documentation/usage) — Adding summaries, tags, and custom responses # File Uploads When you configure file uploads on a route, Arkos automatically generates proper `multipart/form-data` OpenAPI documentation — no manual schema writing required. ## Automatic Generation [#automatic-generation] Define file uploads in your route config, and Arkos handles the rest. ### Custom Routes (ArkosRouter) [#custom-routes-arkosrouter] ```ts title="src/modules/product/product.router.ts" import { ArkosRouter } from "arkos"; import z from "zod"; const router = ArkosRouter(); const CreateProductSchema = z.object({ name: z.string(), price: z.number(), description: z.string().optional(), }); router.post( { path: "/api/products", validation: { body: CreateProductSchema, }, experimental: { uploads: { type: "fields", fields: [ { name: "thumbnail", maxCount: 1 }, { name: "gallery", maxCount: 5 }, ], required: true, }, }, }, productController.create ); ``` ### Built-in Routes (RouteHook) [#built-in-routes-routehook] The same `uploads` configuration works for built-in routes via [RouteHook](/docs/core-concepts/components/route-hooks). `RouteHook` is the new name for `export const config: RouterConfig`. Existing code using the old name still works but will log a deprecation warning. See the [Route Hook guide](/docs/core-concepts/components/route-hooks) for details. ```ts title="src/modules/product/product.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import z from "zod"; export const hook: RouteHook = { createOne: { validation: { body: z.object({ name: z.string(), price: z.number(), }), }, experimental: { uploads: { type: "single", field: "image", required: true, }, }, }, }; const productRouter = ArkosRouter(); export default productRouter; ``` ## What Gets Documented [#what-gets-documented] Arkos automatically: * Merges your validation schema with upload fields * Generates `multipart/form-data` content type * Converts nested validation fields to bracket notation (`user[name]`, `tags[0]`) * Marks file fields as `format: binary` * Sets `maxItems` for array uploads based on `maxCount` **Validation schema fields** become form fields with their original types: | Validation Field | Becomes in OpenAPI | | --------------------------- | ----------------------------------- | | `name: z.string()` | `name` — string field | | `price: z.number()` | `price` — number field | | `tags: z.array(z.string())` | `tags[0]`, `tags[1]` — array fields | **Upload fields** become file fields with binary format: | Upload Config | Becomes in OpenAPI | | ------------------------------- | ---------------------------- | | `field: "thumbnail"` | `thumbnail` — file (binary) | | `fields: [{ name: "gallery" }]` | `gallery[]` — array of files | ## Validation Order [#validation-order] File validation happens before request body validation. Do **not** include upload field names in your validation schema. ```ts // ✅ CORRECT validation: { body: z.object({ name: z.string(), // text field only price: z.number(), }), }, uploads: { type: "single", field: "thumbnail", // file field — separate from validation } // ❌ INCORRECT validation: { body: z.object({ name: z.string(), thumbnail: z.any(), // will fail — file already validated }), }, ``` The generated OpenAPI spec shows a unified `multipart/form-data` request body, but at runtime validation is sequential: files first, then text fields. ## Manual Override [#manual-override] For full control over the OpenAPI schema, define `requestBody` manually: ```ts router.post( { path: "/api/products", experimental: { uploads: { type: "single", field: "image", required: true, }, openapi: { requestBody: { content: { "multipart/form-data": { schema: { type: "object", required: ["image", "name"], properties: { name: { type: "string", description: "Product name" }, image: { type: "string", format: "binary", description: "Product image file", }, }, }, }, }, }, responses: { 201: ProductSchema, }, }, }, }, productController.create ); ``` Arkos validates that your manual `requestBody` matches your uploads configuration. Startup fails with detailed errors if they don't align. ## Next Steps [#next-steps] * [Authentication Integration](/docs/guides/open-api-documentation/integrations/authentication) — Document auth endpoints and security schemes * [Prisma Integration](/docs/guides/open-api-documentation/integrations/prisma) — How query options shape generated schemas * [Documenting Routes](/docs/guides/open-api-documentation/usage) — Adding summaries, tags, and custom responses # Prisma For built-in Prisma model routes without explicit validation schemas, Arkos generates OpenAPI schemas directly from your Prisma models. Custom query options shape what fields appear in the documentation. ## How It Works [#how-it-works] When you don't provide a validation schema for a Prisma model route, Arkos introspects your model and generates a schema based on: 1. **The model structure** — all scalar fields from your Prisma schema 2. **Query options** — `select` and `include` from your `[model].query.ts` file 3. **Relations** — nested related models when included via `include` ## Query Options Integration [#query-options-integration] Your `[model].query.ts` file controls both runtime behavior and OpenAPI documentation: ```ts title="src/modules/post/post.query.ts" import { PrismaQueryOptions } from "arkos/prisma"; import { Prisma } from "@prisma/client"; const postQueryOptions: PrismaQueryOptions = { findMany: { select: { id: true, title: true, excerpt: true, publishedAt: true, author: { select: { id: true, name: true, email: true, }, }, tags: { select: { id: true, name: true, }, }, }, where: { published: true }, orderBy: { publishedAt: "desc" }, }, findOne: { include: { author: true, comments: { where: { approved: true }, orderBy: { createdAt: "asc" }, include: { author: true, }, }, tags: true, }, }, }; export default postQueryOptions; ``` The generated OpenAPI schema for `GET /api/posts` will show only the fields selected in `findMany.select` — exactly what your API returns. For `GET /api/posts/:id`, it shows the full `include` structure with nested relations. ## Schema Generation Rules [#schema-generation-rules] | Source | What Gets Documented | | ------------------------------- | --------------------------------------- | | No validation, no query options | All scalar fields from the Prisma model | | Validation schema present | The validation schema (full control) | | Query options with `select` | Only selected fields | | Query options with `include` | Selected fields + included relations | Query options affect documentation **only for built-in routes without validation schemas**. If you provide a validation schema, it takes precedence for both validation and documentation. ## Relations in Documentation [#relations-in-documentation] When your query options include relations, Arkos documents the full nested structure: ```ts // Response schema in OpenAPI will show: { id: string, title: string, author: { id: string, name: string, email: string }, tags: [ { id: string, name: string } ] } ``` ## Overriding Generated Schemas [#overriding-generated-schemas] To take full control of documentation for a specific endpoint, add a validation schema and `openapi` config: ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import z from "zod"; export const hook: RouteHook = { findMany: { validation: { query: z.object({ published: z.boolean().optional(), tag: z.string().optional(), }), }, experimental: { openapi: { summary: "List published posts", description: "Get paginated list of posts with optional tag filtering", responses: { 200: z.array(CustomPostSchema), }, }, }, }, }; const router = ArkosRouter(); export default router; ``` `RouteHook` is the new name for `export const config: RouterConfig`. Existing code using the old name still works but will log a deprecation warning. See the [Route Hook guide](/docs/core-concepts/components/route-hooks) for details. Once you add `validation` or `openapi.responses`, Arkos uses your definitions instead of generating from query options. ## Next Steps [#next-steps] * [File Uploads Integration](/docs/guides/open-api-documentation/integrations/file-uploads) — Automatic multipart/form-data documentation * [Authentication Integration](/docs/guides/open-api-documentation/integrations/authentication) — Document auth endpoints and security schemes * [Documenting Routes](/docs/guides/open-api-documentation/usage) — Adding summaries, tags, and custom responses # Migration Guide This guide covers migrating from JSDoc-based OpenAPI documentation (v1.3 and earlier) to the declarative `openapi` config approach introduced in v1.4. ## Why Migrate [#why-migrate] | JSDoc Approach | Declarative Approach | | ------------------------- | ---------------------------- | | Manual JSDoc comments | Type-safe config objects | | String-based references | Direct schema references | | No validation integration | Reuses validation schemas | | Error-prone | Compile-time checking | | Separate from route logic | Co-located with route config | ## Migration Steps [#migration-steps] ### 1. Update Configuration [#1-update-configuration] ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ swagger: { endpoint: "/api/docs", options: { definition: { openapi: "3.1.0", info: { title: "My API", version: "1.0.0", }, }, }, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { swagger: { endpoint: "/api/docs", options: { definition: { openapi: "3.1.0", info: { title: "My API", version: "1.0.0", }, }, }, }, }; export default arkosConfig; ``` ```ts title="src/app.ts" import arkos from "arkos"; arkos.init({ swagger: { endpoint: "/api/docs", options: { definition: { openapi: "3.1.0", info: { title: "My API", version: "1.0.0", }, }, }, }, }); ``` ### 2. Migrate Custom Routes [#2-migrate-custom-routes] **Before (JSDoc):** ```ts title="src/routers/user.router.ts" import { Router } from "express"; const router = Router(); /** * @swagger * /api/users/{id}: * get: * summary: Get user by ID * tags: [Users] * parameters: * - in: path * name: id * required: true * schema: * type: string * responses: * 200: * description: User found * content: * application/json: * schema: * $ref: '#/components/schemas/User' * 404: * description: User not found */ router.get("/api/users/:id", userController.getUser); export default router; ``` **After (ArkosRouter):** ```ts title="src/modules/user/user.router.ts" import { ArkosRouter } from "arkos"; import z from "zod"; import userController from "./user.controller"; const router = ArkosRouter(); const UserSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email(), }); const ErrorSchema = z.object({ message: z.string(), }); router.get( { path: "/api/users/:id", validation: { params: z.object({ id: z.string().uuid() }), }, experimental: { openapi: { summary: "Get user by ID", tags: ["Users"], responses: { 200: UserSchema, 404: { content: ErrorSchema, description: "User not found", }, }, }, }, }, userController.getUser ); export default router; ``` ### 3. Migrate Built-in Route Documentation [#3-migrate-built-in-route-documentation] **Before (JSDoc):** ```ts title="src/modules/user/user.router.ts" import { Router } from "express"; import { RouteHook } from "arkos"; export const config: RouteHook = { // configuration }; const router = Router(); /** * @swagger * /api/users: * get: * summary: List users * tags: [Users] * responses: * 200: * description: Users retrieved * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/User' */ router.get("/api/users", userController.list); export default router; ``` **After (RouteHook):** ```ts title="src/modules/user/user.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import z from "zod"; export const hook: RouteHook = { findMany: { experimental: { openapi: { summary: "List users", tags: ["Users"], responses: { 200: z.array(UserSchema), }, }, }, }, }; const router = ArkosRouter(); export default router; ``` ### 4. Migrate Authentication Endpoints [#4-migrate-authentication-endpoints] **Before (JSDoc):** ```ts /** * @swagger * /api/auth/login: * post: * summary: User login * tags: [Authentication] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * email: * type: string * password: * type: string * responses: * 200: * description: Login successful * content: * application/json: * schema: * type: object * properties: * token: * type: string * 401: * description: Invalid credentials */ ``` **After (RouterConfig):** ```ts title="src/modules/auth/auth.router.ts" import { ArkosRouter, RouterConfig } from "arkos"; import z from "zod"; export const config: RouterConfig<"auth"> = { login: { validation: { body: z.object({ email: z.string().email(), password: z.string().min(6), }), }, experimental: { openapi: { summary: "User login", tags: ["Authentication"], responses: { 200: { content: z.object({ token: z.string() }), description: "Login successful", }, 401: { content: z.object({ message: z.string() }), description: "Invalid credentials", }, }, }, }, }, }; const router = ArkosRouter(); export default router; ``` ## Key Differences [#key-differences] | Feature | JSDoc | Declarative | | ----------------- | --------------------------- | ----------------------------------- | | Schema definition | `$ref` or manual properties | Direct Zod/class-validator schemas | | Parameter docs | Manual `parameters` array | Auto from `validation.query/params` | | Request body | Manual `requestBody` | Auto from `validation.body` | | Responses | Manual schema references | Direct schema objects | | Security | Manual `security` array | Auto from `authentication` config | | Type safety | None | Full TypeScript inference | ## Breaking Changes [#breaking-changes] 1. **Schemas must be defined inline or imported** — No more `$ref` strings 2. **Route config replaces JSDoc** — All documentation moves into `experimental.openapi` 3. **ArkosRouter required** — Custom routes must use `ArkosRouter` instead of Express `Router` 4. **Validation schemas drive input docs** — Request body, query, and params are documented from `validation` config ## Rollback [#rollback] If you need to revert, keep both approaches temporarily: ```ts // Can co-exist during migration router.get( { path: "/api/users/:id", experimental: { openapi: { /* new way */ }, }, }, userController.getUser ); // Old JSDoc still works alongside /** * @swagger * /api/users: * get: * summary: List users */ router.get("/api/users", userController.list); ``` Once all routes are migrated, remove the JSDoc comments and update your config. ## Related Guides [#related-guides] * [Documenting Routes](/docs/guides/open-api-documentation/usage) — Full reference for the declarative approach * [Prisma Integration](/docs/guides/open-api-documentation/integrations/prisma) — Auto-generating docs from models * [File Uploads Integration](/docs/guides/open-api-documentation/integrations/file-uploads) — Documenting multipart forms * [Authentication Integration](/docs/guides/open-api-documentation/integrations/authentication) — Securing your docs # Setup Arkos automatically generates OpenAPI documentation for your API — no manual schemas, no JSDoc blocks, no mode selection. Validation schemas drive the docs, and Prisma models fill in the gaps. Once configured, visit `/api/docs` to view your interactive documentation. ## Configuration [#configuration] ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ swagger: { endpoint: "/api/docs", options: { definition: { openapi: "3.1.0", info: { title: "My API", version: "1.0.0", }, servers: [ { url: "http://localhost:8000", description: "Development", }, ], }, }, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { swagger: { endpoint: "/api/docs", options: { definition: { openapi: "3.1.0", info: { title: "My API", version: "1.0.0", }, }, }, }, }; export default arkosConfig; ``` ```ts title="src/app.ts" import arkos from "arkos"; arkos.init({ swagger: { endpoint: "/api/docs", options: { definition: { openapi: "3.1.0", info: { title: "My API", version: "1.0.0", }, }, }, }, }); ``` | Option | Description | | ----------------------------------------------- | ------------------------------------------------------------- | | `endpoint` | URL path where documentation is served (default: `/api/docs`) | | `options.definition` | Standard OpenAPI specification object | | `options.definition.components.securitySchemes` | Define authentication schemes for your API | ## Scalar UI [#scalar-ui] Arkos uses [Scalar](https://scalar.com) as the API documentation UI. Customize its appearance: ```ts title="arkos.config.ts" export default defineConfig({ swagger: { // ... other config scalarApiReferenceConfiguration: { theme: "deepSpace", // or "default", "alternate", "saturn" darkMode: true, layout: "modern", customCss: ` .scalar-app { font-family: 'Inter', sans-serif; } `, }, }, }); ``` **Available themes:** `default`, `alternate`, `deepSpace`, `saturn`, `kepler` ## How It Works [#how-it-works] Arkos builds your OpenAPI spec from two sources: 1. **Validation schemas** — When you add `validation` to a route (custom or built-in), Arkos converts your Zod schemas or class-validator DTOs to OpenAPI schemas 2. **Prisma models** — For built-in routes without validation, Arkos generates schemas directly from your Prisma models and [query options](/docs/guides/open-api-documentation/integrations/prisma) No mode. No manual work. Just configure and go. ## Next Steps [#next-steps] * [Documenting Routes](/docs/guides/open-api-documentation/usage) — Add summaries, descriptions, and custom responses * [Prisma Integration](/docs/guides/open-api-documentation/integrations/prisma) — How query options shape generated schemas * [File Uploads Integration](/docs/guides/open-api-documentation/integrations/file-uploads) — Automatic multipart/form-data documentation * [Authentication Integration](/docs/guides/open-api-documentation/integrations/authentication) — Document auth endpoints and security schemes * [Migration Guide](/docs/guides/open-api-documentation/migration) — Upgrade from JSDoc to ArkosRouter # Usage Documentation happens automatically — validation schemas become OpenAPI schemas, Prisma models fill the gaps. But when you need summaries, descriptions, custom responses, or tags, add an `openapi` config to your route. Both custom routes (`ArkosRouter`) and built-in routes (`RouterConfig`) use the same `openapi` structure. ## ArkosRouter (Custom Routes) [#arkosrouter-custom-routes] For custom routes, add `openapi` under `experimental` in your route config: ```ts title="src/modules/post/post.router.ts" import { ArkosRouter } from "arkos"; import z from "zod"; import postController from "./post.controller"; const router = ArkosRouter(); const PostResponseSchema = z.object({ id: z.string(), title: z.string(), content: z.string(), published: z.boolean(), author: z.object({ id: z.string(), name: z.string(), }), }); const ErrorSchema = z.object({ message: z.string(), code: z.number(), }); router.get( { path: "/api/posts/:id", validation: { params: z.object({ id: z.string().uuid() }), }, experimental: { openapi: { summary: "Get post by ID", description: "Retrieve a specific post with its author details", tags: ["Posts"], responses: { 200: PostResponseSchema, 404: { content: ErrorSchema, description: "Post not found", }, 401: { content: ErrorSchema, description: "Authentication required", }, }, }, }, }, postController.getPost ); export default router; ``` ```ts title="src/modules/post/post.router.ts" import { ArkosRouter } from "arkos"; import { IsUUID } from "class-validator"; import postController from "./post.controller"; class PostParamsDto { @IsUUID() id: string; } class PostResponseDto { id: string; title: string; content: string; published: boolean; author: { id: string; name: string; }; } router.get( { path: "/api/posts/:id", validation: { params: PostParamsDto, }, experimental: { openapi: { summary: "Get post by ID", description: "Retrieve a specific post with its author details", tags: ["Posts"], responses: { 200: PostResponseDto, 404: { content: class ErrorDto { message: string; code: number; }, description: "Post not found", }, }, }, }, }, postController.getPost ); export default router; ``` ## RouteHook (Built-in Routes) [#routehook-built-in-routes] For built-in routes (Prisma models, authentication, file uploads), use the same `openapi` structure inside your `hook` export. `RouteHook` is the new name for `export const config: RouterConfig`. Existing code using the old name still works but will log a deprecation warning. See the [Route Hook guide](/docs/core-concepts/components/route-hooks) for details. ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import z from "zod"; export const hook: RouteHook = { findMany: { experimental: { openapi: { summary: "List posts", description: "Get paginated list of posts with filtering options", tags: ["Posts"], responses: { 200: z.array(PostResponseSchema), }, }, }, }, findOne: { experimental: { openapi: { summary: "Get post", tags: ["Posts"], responses: { 200: PostResponseSchema, 404: { content: ErrorSchema, description: "Post not found" }, }, }, }, }, createOne: { experimental: { openapi: { summary: "Create post", tags: ["Posts"], responses: { 201: PostResponseSchema, 400: { content: ErrorSchema, description: "Validation failed" }, }, }, }, }, }; const router = ArkosRouter(); export default router; ``` The `openapi` config object is identical for `ArkosRouter` and `RouteHook`. The same structure works across Prisma model routes, authentication routes, and file upload routes. ## Response Shortcuts [#response-shortcuts] You can pass schemas directly — Arkos converts them to OpenAPI schemas automatically: ```ts experimental: { openapi: { responses: { 200: UserSchema, // Shortcut — schema only 201: { content: UserSchema, description: "Created" }, // With description 404: ErrorSchema, // Shortcut works for any status 500: { description: "Server error" }, // No content schema }, }, } ``` For arrays, wrap in `z.array()`: ```ts responses: { 200: z.array(UserSchema), } ``` ## Authentication & Security [#authentication--security] Reference security schemes defined in your OpenAPI config: ```ts router.get( { path: "/api/users/me", authentication: true, // Arkos auth — automatically documented experimental: { openapi: { summary: "Get current user", security: [{ BearerAuth: [] }], // Optional: override or add multiple schemes responses: { 200: UserSchema, 401: { content: ErrorSchema, description: "Not authenticated" }, }, }, }, }, userController.getMe ); ``` When using Arkos's built-in `authentication` config, the security scheme is automatically documented. The `security` field is only needed for custom security scenarios. ## Query & Path Parameters [#query--path-parameters] Parameters from `validation.query` and `validation.params` are automatically documented: ```ts router.get( { path: "/api/posts", validation: { query: z.object({ published: z.boolean().optional(), authorId: z.string().uuid().optional(), limit: z.coerce.number().min(1).max(100).default(20), }), }, experimental: { openapi: { summary: "List posts", tags: ["Posts"], responses: { 200: z.array(PostResponseSchema), }, }, }, }, postController.listPosts ); ``` This generates query parameters in OpenAPI with proper types, descriptions (from Zod's `.describe()`), and default values. ## Request Body [#request-body] Request bodies from `validation.body` are automatically documented: ```ts router.post( { path: "/api/posts", validation: { body: z.object({ title: z.string().min(1).describe("Post title"), content: z.string().min(1).describe("Post content"), published: z.boolean().default(false), }), }, experimental: { openapi: { summary: "Create post", tags: ["Posts"], responses: { 201: PostResponseSchema, }, }, }, }, postController.createPost ); ``` ## Next Steps [#next-steps] * [Prisma Integration](/docs/guides/open-api-documentation/integrations/prisma) — How query options shape generated schemas * [File Uploads Integration](/docs/guides/open-api-documentation/integrations/file-uploads) — Automatic multipart/form-data documentation * [Authentication Integration (v1.6+)](/docs/guides/open-api-documentation/integrations/authentication) — Document auth endpoints and security schemes * [Migration Guide](/docs/guides/open-api-documentation/migration) — Upgrade from JSDoc to ArkosRouter # Authentication Authentication security in Arkos is about hardening how tokens are created, how long they live, and how they travel between client and server. Getting this right is critical — a weak secret or misconfigured cookie can expose your entire user base. ## JWT Secret [#jwt-secret] The JWT secret is used to sign and verify every access token in your application. If it's compromised, anyone can forge valid tokens for any user. **Rules:** * Use a long, random string — at least 32 characters, ideally 64+ * Never commit it to source control * Set it via environment variable in production: `JWT_SECRET` * Arkos will refuse to start if `JWT_SECRET` is not set when running `npx arkos start` ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ authentication: { mode: "static", jwt: { secret: process.env.JWT_SECRET, expiresIn: "15m", // short-lived tokens are safer }, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { authentication: { mode: "static", jwt: { secret: process.env.JWT_SECRET, expiresIn: "15m", }, }, }; export default arkosConfig; ``` The config value takes precedence over the environment variable. If you hardcode the secret in `arkos.config.ts` and commit that file, your secret is exposed regardless of what's in `.env`. *** ## Token Expiry [#token-expiry] Short-lived tokens reduce the window of exposure if a token is stolen. The tradeoff is that users need to re-authenticate more frequently — balance this against your application's UX requirements. | Use case | Recommended `expiresIn` | | ----------------------------------- | ----------------------- | | High-security apps (banking, admin) | `15m` – `1h` | | Standard web apps | `1d` – `7d` | | Mobile apps with refresh tokens | `15m` with refresh flow | *** ## Cookie Flags [#cookie-flags] When Arkos sends the JWT as a cookie, three flags control its security: | Flag | Default | What it does | | ---------- | -------------------------------- | -------------------------------------------------------------------- | | `httpOnly` | `true` | Prevents JavaScript from reading the cookie — blocks XSS token theft | | `secure` | `true` in production | Only sends the cookie over HTTPS | | `sameSite` | `"lax"` in dev, `"none"` in prod | Controls cross-site cookie sending | ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ authentication: { mode: "static", jwt: { cookie: { httpOnly: true, secure: true, sameSite: "strict", // tightest option — only same-site requests }, }, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { authentication: { mode: "static", jwt: { cookie: { httpOnly: true, secure: true, sameSite: "strict", }, }, }, }; export default arkosConfig; ``` Use `sameSite: "none"` only if your frontend and API are on different domains and you need cookies to be sent cross-site. It requires `secure: true`. *** ## Superuser [#superuser] Arkos's built-in authorization checks are bypassed entirely for users where `isSuperUser === true`. A superuser can perform any action on any resource regardless of their role or the route's access control rules. Assign `isSuperUser` only to accounts that genuinely need unrestricted access — typically a single system administrator account. Never expose an endpoint that allows users to set this field on themselves. ```ts title="src/modules/user/user.interceptors.ts" import { AppError } from "arkos/error-handler"; import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; // Prevent anyone from setting isSuperUser through the API export const beforeUpdateOne = [ async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { if ("isSuperUser" in req.body) { throw new AppError("Field not allowed", 400, "ForbiddenField"); } next(); }, ]; ``` For full authentication setup see [Authentication — Setup](/docs/core-concepts/authentication/setup). # Infrastructure Infrastructure security controls what reaches your server before any route handler or business logic runs. Arkos ships CORS, Helment and rate limiting out of the box. ## CORS [#cors] CORS controls which origins are allowed to make requests to your API. By default Arkos blocks all origins unless you explicitly allow them. **Avoid at all cost `allowedOrigins: "*"` in production.** It allows any website to make requests to your API on behalf of your users. ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ middlewares: { cors: { allowedOrigins: ["https://myapp.com", "https://admin.myapp.com"], options: { credentials: true }, }, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { middlewares: { cors: { allowedOrigins: ["https://myapp.com", "https://admin.myapp.com"], options: { credentials: true }, }, }, }; export default arkosConfig; ``` `credentials: true` is required if your frontend sends cookies or `Authorization` headers. Without it, browsers will block credentialed cross-origin requests even if the origin is allowed. For full CORS configuration options see [Global Middlewares — CORS](/docs/guides/global-middlewares#cors). *** ## Helmet [#helmet] > Available since v1.6.0-beta Helmet sets HTTP security headers on every response — `X-Content-Type-Options`, `X-Frame-Options`, `Strict-Transport-Security`, `Content-Security-Policy`, and others. These headers protect against a wide range of browser-based attacks including clickjacking, MIME sniffing, and cross-site scripting. ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ middlewares: { helmet: {}, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; import helmet from "helmet"; const arkosConfig: ArkosConfig = { configureApp: (app) => { app.use(helmet()); }, }; export default arkosConfig; ``` Install helmet first: `npm install helmet` or `pnpm add helmet`. See the [helmet npm package](https://www.npmjs.com/package/helmet) for the full list of headers it sets and how to configure each one. *** ## Rate Limiting [#rate-limiting] Arkos ships two rate limiting layers: **Global rate limit** — applies to every endpoint. Protects your server from general abuse and scraping. **Auth rate limit** — applies only to authentication endpoints (`/api/auth/login`, `/api/auth/signup`, etc.). Tighter by default because these endpoints are the primary target for brute force attacks. ### Global Rate Limit [#global-rate-limit] **Default:** ```ts { windowMs: 60 * 1000, // 1 minute limit: 300, } ``` ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ middlewares: { rateLimit: { windowMs: 60 * 1000, limit: 100, }, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { middlewares: { rateLimit: { windowMs: 60 * 1000, limit: 100, }, }, }; export default arkosConfig; ``` ### Auth Rate Limit [#auth-rate-limit] **Default:** ```ts { windowMs: 5000, // 5 seconds limit: 10, } ``` ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ authentication: { mode: "static", requestRateLimitOptions: { windowMs: 15 * 60 * 1000, // 15 minutes limit: 5, }, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { authentication: { mode: "static", requestRateLimitOptions: { windowMs: 15 * 60 * 1000, limit: 5, }, }, }; export default arkosConfig; ``` You can also set rate limits per route using `ArkosRouter`. See [ArkosRouter](/docs/reference/arkos-router) for details. The default global limit of 300 requests per minute is permissive — tune it down for production based on your expected traffic patterns. # Overview Security in Arkos is layered. No single config option makes your API secure — it's the combination of transport controls, authentication hardening, input validation, and production configuration working together. This guide walks through each layer and what Arkos gives you to lock it down. ## The Layers [#the-layers] **Infrastructure** — controls what reaches your server at all. CORS decides which origins can talk to your API. Helmet sets security headers on every response. Rate limiting caps how much any single client can send. These run before any of your code touches the request. **Authentication** — controls who can access protected routes. JWT configuration determines how tokens are signed, how long they live, and how they travel between client and server. Auth rate limiting protects login and signup endpoints specifically. **Validation** — controls what data gets into your application. Schema validation, unknown field rejection, and route-level strict mode ensure malformed or unexpected input never reaches your business logic. **Production** — configuration that must be correct before you go live. Missing secrets, open CORS, permissive rate limits, and disabled security headers are the most common causes of production security incidents. ## What's In Each Section [#whats-in-each-section] * [Infrastructure](/docs/guides/security/infrastructure) — CORS, Helmet, rate limiting * [Authentication](/docs/guides/security/authentication) — JWT hardening, cookie flags, auth rate limiting, superuser risks * [Validation](/docs/guides/security/validation) — `forbidNonWhitelisted`, `ValidatorOptions`, strict mode, disabling unused endpoints * [Production](/docs/guides/security/production) — startup requirements, environment behavior, production checklist # Production Before going live, there are a set of security requirements that must be in place. Some of these Arkos enforces automatically — others are your responsibility to configure correctly. ## Environment Behavior [#environment-behavior] Arkos determines its environment through its CLI commands, not `NODE_ENV`: * **`npx arkos dev`** — development mode. Full error details exposed including stack traces. Never run this in production. * **`npx arkos start`** — production mode. Error responses are sanitized. Stack traces hidden. Non-operational errors return a generic message. PS: it must be ran after `npx arkos build`. Running `npx arkos dev` in a production environment exposes stack traces and internal error details to clients. Always use `npx arkos start` in production. *** ## JWT Secret [#jwt-secret] Arkos will **refuse to start** if you've set authentication and `JWT_SECRET` is not set when running `npx arkos start`. This is intentional — a missing secret means every token in your application is insecure. Set it as an environment variable: ```bash JWT_SECRET=your-long-random-secret-here ``` Or in your config (environment variable is preferred — never commit secrets): ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ authentication: { mode: "static", jwt: { secret: process.env.JWT_SECRET, }, }, }); ``` Generate a strong secret: ```bash node -e "console.log(require('crypto').randomBytes(64).toString('hex'))" ``` *** ## Production Checklist [#production-checklist] Go through this before every production deployment: **Infrastructure** * [ ] CORS `allowedOrigins` set to specific domains — avoid `"*"` * [ ] Global rate limit tuned down from the permissive default * [ ] Auth rate limit configured for your login endpoint **Authentication** * [ ] `JWT_SECRET` set as an environment variable — not hardcoded * [ ] `jwt.expiresIn` set to an appropriate short duration * [ ] `jwt.cookie.httpOnly` is `true` * [ ] `jwt.cookie.secure` is `true` * [ ] `jwt.cookie.sameSite` is `"strict"` or `"lax"` — not `"none"` unless required * [ ] `isSuperUser` is not assignable through any public endpoint **Validation** * [ ] `forbidNonWhitelisted: true` (default — verify it hasn't been disabled) * [ ] `routers.strict` set to `"no-bulk"` or `true` if bulk endpoints aren't needed (Must equal development to avoid mismatch) * [ ] Strict route validation enabled if your routes should have no unvalidated inputs **Environment** * [ ] Running `npx arkos build` and then `npx arkos start` — not `npx arkos dev` * [ ] `DATABASE_URL` set as an environment variable * [ ] No secrets committed to source control * [ ] `.env` in `.gitignore` # Validation Input validation is your last line of defense before untrusted data reaches your database. Arkos provides several mechanisms to ensure only expected, well-shaped data gets through. ## Unknown Field Rejection [#unknown-field-rejection] By default Arkos rejects unknown fields on all validated inputs. This prevents mass assignment — a class of vulnerability where an attacker sends extra fields (like `isAdmin: true`) that slip through into a database write. Both resolvers ship with `forbidNonWhitelisted: true` by default: ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ validation: { resolver: "zod", validationOptions: { forbidNonWhitelisted: true, // default — shown for clarity }, }, }); ``` ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ validation: { resolver: "class-validator", validationOptions: { whitelist: true, // strips unknown fields forbidNonWhitelisted: true, // throws instead of stripping }, }, }); ``` Avoid `forbidNonWhitelisted: false` in your applications at all costs. Silently ignoring unknown fields might seem harmless but creates a surface for mass assignment attacks. ### Class Validator — Full ValidatorOptions [#class-validator--full-validatoroptions] When using `class-validator`, `validationOptions` accepts the full [`ValidatorOptions`](https://github.com/typestack/class-validator#passing-options) interface. Beyond `forbidNonWhitelisted`, useful security-relevant options include: | Option | Default | Effect | | ----------------------- | ------- | --------------------------------------------------------- | | `whitelist` | `true` | Strips properties not decorated with any validator | | `forbidNonWhitelisted` | `true` | Throws instead of stripping unknown properties | | `forbidUnknownValues` | — | Throws when validating unknown object types | | `skipMissingProperties` | `false` | When `false`, missing required properties fail validation | *** ## Strict Route Validation [#strict-route-validation] Strict mode requires every route to explicitly declare its validation intent. Without it, routes with no `validation` config silently accept any input. ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ validation: { resolver: "zod", strict: true, }, }); ``` In strict mode each of `body`, `query`, and `params` must be explicitly declared: | Value | Behavior | | --------------------------------- | --------------------------------------- | | `ZodSchema` \| `ClassConstructor` | Validates input | | `false` | Allows input through without validation | | `null` \| `undefined` \| not set | Not allowed — returns 400 | See [Validation — Setup](/docs/guides/validation/setup) for the full strict mode behavior. *** ## Disabling Unused Endpoints [#disabling-unused-endpoints] Arkos auto-generates CRUD endpoints for every Prisma model. In production, exposing endpoints you don't use — especially bulk operations like `deleteMany` — is an unnecessary attack surface. Use `routers.strict` to control this: ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; export default defineConfig({ routers: { strict: "no-bulk", // disables createMany, updateMany, deleteMany globally // strict: true, // disables ALL auto-generated endpoints }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { routers: { strict: "no-bulk", }, }; export default arkosConfig; ``` | Value | Effect | | ----------- | ----------------------------------------------------------------------------- | | `false` | All endpoints enabled (default) | | `"no-bulk"` | Bulk operations disabled globally | | `true` | All auto-generated endpoints disabled — must enable per model via `RouteHook` | `routers.strict: true` combined with explicit `RouteHook` configuration per model gives you the principle of least privilege — no endpoint exists unless you deliberately enabled it. For full details on enabling endpoints per model see [Route Hook](/docs/core-concepts/components/route-hooks). # Custom Resolver This will help you use custom resolvers for example JOI and others, still under discussion if it is going to be useful leave your thoughts at [Arkos' Github](https://github.com/uanela/arkos). # Helper Functions Arkos provides two helper functions for manual validation outside the declarative `validation` config — useful in interceptors, services, or anywhere you need to validate arbitrary data imperatively. ## validateSchema [#validateschema] Validates data against a Zod schema. ```ts title="src/modules/post/post.interceptors.ts" import { validateSchema } from "arkos/validation"; import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; import z from "zod"; const BulkImportSchema = z.array( z.object({ title: z.string().min(1), content: z.string().min(1), authorEmail: z.string().email(), }) ); export const beforeCreateMany = [ async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const validated = await validateSchema(BulkImportSchema, req.body); req.body = validated; next(); }, ]; ``` ```ts title="src/modules/post/post.controller.ts" import { validateSchema } from "arkos/validation"; import { ArkosRequest, ArkosResponse } from "arkos"; import { BaseController } from "arkos"; import z from "zod"; import postService from "./post.service"; const BulkImportSchema = z.array( z.object({ title: z.string().min(1), content: z.string().min(1), authorEmail: z.string().email(), }) ); class PostController extends BaseController { async bulkImport(req: ArkosRequest, res: ArkosResponse) { const validated = await validateSchema(BulkImportSchema, req.body); // validated is now fully typed and safe req.body = validated; } } export default new PostController(postService); ``` **Return value:** Returns the validated data (type-safe, with transformations applied). Throws a validation error if validation fails. ## validateDto [#validatedto] Validates data against a class-validator DTO. ```ts title="src/modules/user/user.interceptors.ts" import { validateDto } from "arkos/validation"; import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; import CreateUserDto from "./dtos/create-user.dto"; export const beforeCreateOne = [ async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const validated = await validateDto(CreateUserDto, req.body); req.body = validated; next(); }, ]; ``` ```ts title="src/modules/user/user.controller.ts" import { validateDto } from "arkos/validation"; import { ArkosRequest, ArkosResponse } from "arkos"; import { BaseController } from "arkos"; import CreateUserDto from "./dtos/create-user.dto"; import userService from "./user.service"; class UserController extends BaseController { async createUser(req: ArkosRequest, res: ArkosResponse) { const validated = await validateDto(CreateUserDto, req.body); req.body = validated; } } export default new UserController(userService); ``` **Return value:** Returns the validated DTO instance (with transformed properties). Throws a validation error if validation fails. ## Error Handling [#error-handling] Both helpers throw validation errors with the same structure as declarative validation: ```json { "status": "error", "message": "Invalid Data", "code": 400, "errors": [ { "property": "email", "constraints": { "isEmail": "email must be an email" } } ] } ``` You can let them propagate to Arkos's global error handler, or catch them explicitly: ```ts import { validateSchema } from "arkos/validation"; try { const validated = await validateSchema(Schema, data); } catch (error) { // validation error — already structured, rethrow or handle throw error; } ``` ## Related [#related] * [Setup](/docs/guides/validation/setup) — Enable validation and choose a resolver * [Usage](/docs/guides/validation/usage) — Declarative validation with [ArkosRouter](/docs/core-concepts/components/routers) and [Route Hook](/docs/core-concepts/components/route-hooks) # Setup Arkos ships a validation system that plugs into both [ArkosRouter](/docs/core-concepts/components/routers) and [RouteHook](/docs/core-concepts/components/route-hooks) declaratively — validate `req.body`, `req.query`, and `req.params` with zero boilerplate, with automatic OpenAPI spec generation from your schemas. ## Configuration [#configuration] ```ts title="arkos.config.ts" import { defineConfig } from "arkos"; export default defineConfig({ validation: { resolver: "zod", // or "class-validator" validationOptions: { whitelist: true, }, }, }); ``` ```ts title="arkos.config.ts" import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { validation: { resolver: "zod", validationOptions: { whitelist: true, }, }, }; export default arkosConfig; ``` ```ts title="src/app.ts" import arkos from "arkos"; arkos.init({ validation: { resolver: "zod", validationOptions: { whitelist: true, }, }, }); ``` | Option | Values | Description | | ------------------- | ------------------------------ | --------------------------------------- | | `resolver` | `"zod"` \| `"class-validator"` | Validation library to use | | `validationOptions` | object | Options passed directly to the resolver | | `strict` | `boolean` | Enables strict mode — see below | Validation is disabled by default. Without a `resolver` set, no validation runs on any route. ## Quick Example [#quick-example] Validation works the same way whether you're on a custom route or a built-in one. ```ts title="src/modules/report/report.router.ts" import { ArkosRouter } from "arkos"; import z from "zod"; import reportController from "./report.controller"; const router = ArkosRouter(); router.post( { path: "/api/reports", validation: { body: z.object({ title: z.string().min(1), type: z.enum(["sales", "inventory"]), }), query: z.object({ notify: z.coerce.boolean().optional(), }), }, }, reportController.createReport ); export default router; ``` ```ts title="src/modules/report/report.router.ts" import { ArkosRouter } from "arkos"; import { IsString, IsEnum, IsBoolean, IsOptional, MinLength } from "class-validator"; import { Type } from "class-transformer"; import reportController from "./report.controller"; const router = ArkosRouter(); class CreateReportBodyDto { @IsString() @MinLength(1) title: string; @IsEnum(["sales", "inventory"]) type: string; } class CreateReportQueryDto { @Type(() => Boolean) @IsBoolean() @IsOptional() notify?: boolean; } router.post( { path: "/api/reports", validation: { body: CreateReportBodyDto, query: CreateReportQueryDto, }, }, reportController.createReport ); export default router; ``` ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import z from "zod"; const CreatePostSchema = z.object({ title: z.string().min(1), content: z.string().min(1), published: z.boolean().optional(), authorId: z.string().uuid(), }); export const hook: RouteHook = { createOne: { validation: { body: CreatePostSchema, }, }, }; const router = ArkosRouter(); export default router; ``` ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import { IsString, IsBoolean, IsUUID, IsOptional, MinLength } from "class-validator"; class CreatePostDto { @IsString() @MinLength(1) title: string; @IsString() @MinLength(1) content: string; @IsBoolean() @IsOptional() published?: boolean; @IsUUID() authorId: string; } export const hook: RouteHook = { createOne: { validation: { body: CreatePostDto, }, }, }; const router = ArkosRouter(); export default router; ``` `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](/docs/core-concepts/components/route-hooks) for full details. ## Strict Mode [#strict-mode] By default, routes without a `validation` config let all input through unvalidated. Strict mode inverts this — every route must explicitly declare its validation intent. Enable it in your config: ```ts defineConfig({ validation: { resolver: "zod", strict: true, }, }); ``` In strict mode, each of `body`, `query`, and `params` follows the same rule: | Value | Behavior | | --------------------------------- | -------------------------------------------- | | `ZodSchema` \| `ClassConstructor` | Validates the input against the schema | | `false` | Allows input through without validation | | `null` | Prohibits the input entirely — returns `400` | | `undefined` / not set | Prohibits the input entirely — returns `400` | Without strict mode, omitting a key simply skips validation for that target. ```ts router.get( { path: "/api/reports", validation: { query: ReportQuerySchema, // validated body: false, // explicitly allowed through params: null, // prohibited — 400 RequestParamsNotAllowed }, }, reportController.getReports ); ``` Passing validators on a route without a `resolver` set throws at startup: ``` Trying to pass validators into route GET /api/reports config validation option without choosing a validation resolver under arkos.config.ts ``` In strict mode, routes missing explicit validation intent also throw at startup. ## OpenAPI Integration [#openapi-integration] Schemas and DTOs passed to `validation` automatically generate OpenAPI parameters and request body definitions — no extra configuration needed. See [OpenAPI Documentation](/docs/guides/open-api-documentation/setup) for details. ## What's Next [#whats-next] * [Usage](/docs/guides/validation/usage) — validate `body`, `query`, and `params` on your routes * [Customization](/docs/guides/validation/customization/helper-functions) — `validateDto` / `validateSchema` helpers for manual validation # Usage Validate request data inputs such as `req.body`, `req.query`, and `req.params` declaratively on any route — no middleware boilerplate, no manual error handling. Drop a Zod schema or class-validator DTO into the `validation` config, and Arkos handles the rest: error responses and automatic OpenAPI spec generation. This validation system works the same way across both [ArkosRouter](/docs/core-concepts/components/routers) and [RouteHook](/docs/core-concepts/components/route-hooks). ## Request Body Validation [#request-body-validation] Applied on routes that receive a request body — typically `POST`, `PUT`, and `PATCH`. ```ts title="src/modules/post/post.router.ts" import { ArkosRouter } from "arkos"; import z from "zod"; import postController from "./post.controller"; const router = ArkosRouter(); const CreatePostSchema = z.object({ title: z.string().min(1), content: z.string().min(1), published: z.boolean().optional(), authorId: z.string().uuid(), }); router.post( { path: "/api/posts", validation: { body: CreatePostSchema, }, }, postController.createPost ); export default router; ``` ```ts title="src/modules/post/post.router.ts" import { ArkosRouter } from "arkos"; import { IsString, IsBoolean, IsUUID, IsOptional, MinLength } from "class-validator"; import postController from "./post.controller"; const router = ArkosRouter(); class CreatePostDto { @IsString() @MinLength(1) title: string; @IsString() @MinLength(1) content: string; @IsBoolean() @IsOptional() published?: boolean; @IsUUID() authorId: string; } router.post( { path: "/api/posts", validation: { body: CreatePostDto, }, }, postController.createPost ); export default router; ``` ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import z from "zod"; const CreatePostSchema = z.object({ title: z.string().min(1), content: z.string().min(1), published: z.boolean().optional(), authorId: z.string().uuid(), }); export const hook: RouteHook = { createOne: { validation: { body: CreatePostSchema, }, }, }; const router = ArkosRouter(); export default router; ``` ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import { IsString, IsBoolean, IsUUID, IsOptional, MinLength } from "class-validator"; class CreatePostDto { @IsString() @MinLength(1) title: string; @IsString() @MinLength(1) content: string; @IsBoolean() @IsOptional() published?: boolean; @IsUUID() authorId: string; } export const hook: RouteHook = { createOne: { validation: { body: CreatePostDto, }, }, }; const router = ArkosRouter(); export default router; ``` `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](/docs/core-concepts/components/route-hooks) for full details. **Validation error response:** ```json { "status": "error", "message": "Invalid Data", "code": 400, "errors": [ { "property": "authorId", "constraints": { "isUuid": "authorId must be a valid UUID" } } ] } ``` ## Request Query & Params Validation [#request-query--params-validation] Applied on routes that receive URL query strings or path parameters. Query and params values arrive as strings from the URL. Use `z.coerce` or `@Type()` to cast them to the correct type — or use the [CLI code generation](/docs/tooling/cli/code-generation/core#generate-schemas-and-dtos) which handles this automatically. ```ts title="src/modules/user/user.router.ts" import { ArkosRouter } from "arkos"; import z from "zod"; import userController from "./user.controller"; const router = ArkosRouter(); router.get( { path: "/api/users", validation: { query: z.object({ role: z.enum(["admin", "user"]).optional(), active: z.coerce.boolean().optional(), limit: z.coerce.number().int().min(1).max(100).optional(), }), }, }, userController.getUsers ); router.get( { path: "/api/users/:id", validation: { params: z.object({ id: z.string().uuid("Invalid user ID"), }), }, }, userController.getUser ); router.patch( { path: "/api/users/:id", validation: { params: z.object({ id: z.string().uuid(), }), body: z.object({ name: z.string().min(1).optional(), email: z.string().email().optional(), }), query: z.object({ notify: z.coerce.boolean().optional(), }), }, }, userController.updateUser ); export default router; ``` ```ts title="src/modules/user/user.router.ts" import { ArkosRouter } from "arkos"; import { IsEnum, IsBoolean, IsInt, IsUUID, IsString, IsEmail, IsOptional, Min, Max } from "class-validator"; import { Type } from "class-transformer"; import userController from "./user.controller"; const router = ArkosRouter(); class UserQueryDto { @IsEnum(["admin", "user"]) @IsOptional() role?: string; @Type(() => Boolean) @IsBoolean() @IsOptional() active?: boolean; @Type(() => Number) @IsInt() @Min(1) @Max(100) @IsOptional() limit?: number; } class UserParamsDto { @IsUUID() id: string; } class UpdateUserBodyDto { @IsString() @IsOptional() name?: string; @IsEmail() @IsOptional() email?: string; } class UpdateUserQueryDto { @Type(() => Boolean) @IsBoolean() @IsOptional() notify?: boolean; } router.get( { path: "/api/users", validation: { query: UserQueryDto }, }, userController.getUsers ); router.get( { path: "/api/users/:id", validation: { params: UserParamsDto }, }, userController.getUser ); router.patch( { path: "/api/users/:id", validation: { params: UserParamsDto, body: UpdateUserBodyDto, query: UpdateUserQueryDto, }, }, userController.updateUser ); export default router; ``` ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import z from "zod"; export const hook: RouteHook = { findMany: { validation: { query: z.object({ published: z.coerce.boolean().optional(), limit: z.coerce.number().int().min(1).max(100).optional(), }), }, }, updateOne: { validation: { body: z.object({ title: z.string().min(1).optional(), content: z.string().min(1).optional(), }), query: z.object({ notify: z.coerce.boolean().optional(), }), }, }, }; const router = ArkosRouter(); export default router; ``` ```ts title="src/modules/post/post.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import { IsBoolean, IsInt, IsString, IsOptional, Min, Max, MinLength } from "class-validator"; import { Type } from "class-transformer"; class PostQueryDto { @Type(() => Boolean) @IsBoolean() @IsOptional() published?: boolean; @Type(() => Number) @IsInt() @Min(1) @Max(100) @IsOptional() limit?: number; } class UpdatePostBodyDto { @IsString() @MinLength(1) @IsOptional() title?: string; @IsString() @MinLength(1) @IsOptional() content?: string; } class UpdatePostQueryDto { @Type(() => Boolean) @IsBoolean() @IsOptional() notify?: boolean; } export const hook: RouteHook = { findMany: { validation: { query: PostQueryDto, }, }, updateOne: { validation: { body: UpdatePostBodyDto, query: UpdatePostQueryDto, }, }, }; const router = ArkosRouter(); export default router; ``` `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](/docs/core-concepts/components/route-hooks) for full details. **Validation error response:** ```json { "status": "error", "message": "Invalid Data", "code": 400, "errors": [ { "property": "id", "constraints": { "isUuid": "id must be a valid UUID" } } ] } ``` ## Validation With File Uploads [#validation-with-file-uploads] When combining validation with file uploads, only pass text fields to `validation.body` — file fields are handled separately by the `uploads` config. ```ts router.post( { path: "/api/users/:id/avatar", validation: { params: z.object({ id: z.string().uuid() }), body: z.object({ caption: z.string().optional() }), // no avatar field here — handled by uploads }, experimental: { uploads: { type: "single", field: "avatar", required: true }, }, }, userController.uploadAvatar ); ``` ```ts import { IsUUID, IsString, IsOptional } from "class-validator"; class UploadAvatarParamsDto { @IsUUID() id: string; } class UploadAvatarBodyDto { @IsString() @IsOptional() caption?: string; } router.post( { path: "/api/users/:id/avatar", validation: { params: UploadAvatarParamsDto, body: UploadAvatarBodyDto, // no avatar field here — handled by uploads }, experimental: { uploads: { type: "single", field: "avatar", required: true }, }, }, userController.uploadAvatar ); ``` ```ts title="src/modules/file/file.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import z from "zod"; export const hook: RouteHook = { uploadFile: { validation: { body: z.object({ caption: z.string().optional() }), // no file field here — handled by uploads }, }, }; const router = ArkosRouter(); export default router; ``` ```ts title="src/modules/file/file.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import { IsString, IsOptional } from "class-validator"; class UploadFileBodyDto { @IsString() @IsOptional() caption?: string; } export const hook: RouteHook = { uploadFile: { validation: { body: UploadFileBodyDto, // no file field here — handled by uploads }, }, }; const router = ArkosRouter(); export default router; ``` `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](/docs/core-concepts/components/route-hooks) for full details. | `required` | Behavior | | ---------- | ------------------------------------ | | `true` | Returns `400` if no file is uploaded | | `false` | Proceeds without a file | See [File Upload guide](/docs/guides/file-handling/file-uploads/setup) for full configuration. ## Accessing Validated Data [#accessing-validated-data] Arkos automatically types `req.body`, `req.query`, and `req.params` from your validation schema — every handler and middleware in the route stack shares the same typed `req` with no manual generic declarations needed. ```ts title="src/modules/user/user.router.ts" import { ArkosRouter } from "arkos"; import z from "zod"; import userController from "@/src/modules/user/user.controller"; const router = ArkosRouter(); const UpdateUserBody = z.object({ name: z.string().min(1).optional(), email: z.string().email().optional(), }); const UpdateUserParams = z.object({ id: z.string().uuid(), }); const UpdateUserQuery = z.object({ notify: z.coerce.boolean().optional(), }); router.patch( { path: "/api/users/:id", validation: { params: UpdateUserParams, body: UpdateUserBody, query: UpdateUserQuery, }, }, logMiddleware, // req.params, req.body, req.query all typed here userController.updateOne // and here — same signature, no extra work ); export default router; ``` ```ts title="src/modules/user/user.router.ts" import { ArkosRouter } from "arkos"; import { IsString, IsEmail, IsBoolean, IsUUID, IsOptional } from "class-validator"; import { Type } from "class-transformer"; import userController from "@/src/modules/user/user.controller"; const router = ArkosRouter(); class UpdateUserBodyDto { @IsString() @IsOptional() name?: string; @IsEmail() @IsOptional() email?: string; } class UpdateUserParamsDto { @IsUUID() id: string; } class UpdateUserQueryDto { @Type(() => Boolean) @IsBoolean() @IsOptional() notify?: boolean; } router.patch( { path: "/api/users/:id", validation: { params: UpdateUserParamsDto, body: UpdateUserBodyDto, query: UpdateUserQueryDto, }, }, logMiddleware, userController.updateOne ); export default router; ``` ```ts title="src/modules/auth/auth.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import z from "zod"; const UpdateMeBody = z.object({ name: z.string().min(1).optional(), email: z.string().email().optional(), }); export const hook: RouteHook = { updateMe: { validation: { body: UpdateMeBody, }, }, }; const router = ArkosRouter(); export default router; ``` ```ts title="src/modules/auth/auth.middleware.ts" import { ArkosRequest, ArkosResponse, NextFunction } from "arkos"; import { z } from "zod"; const UpdateMeBody = z.object({ name: z.string().min(1).optional(), email: z.string().email().optional(), }); type UpdateMeBody = z.infer; export const beforeUpdateMe = ( req: ArkosRequest, res: ArkosResponse, next: NextFunction ) => { const { name, email } = req.body; // typed, validated next(); }; ``` ```ts title="src/modules/auth/auth.router.ts" import { ArkosRouter, RouteHook } from "arkos"; import { IsString, IsEmail, IsOptional } from "class-validator"; class UpdateMeBodyDto { @IsString() @IsOptional() name?: string; @IsEmail() @IsOptional() email?: string; } export const hook: RouteHook = { updateMe: { validation: { body: UpdateMeBodyDto, }, }, }; const router = ArkosRouter(); export default router; ``` ```ts title="src/modules/auth/auth.middleware.ts" import { ArkosRequest, ArkosResponse, NextFunction } from "arkos"; export const beforeUpdateMe = ( req: ArkosRequest, res: ArkosResponse, next: NextFunction ) => { const { name, email } = req.body; // typed, validated next(); }; ``` `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](/docs/core-concepts/components/route-hooks) for full details. ```ts title="src/modules/user/user.controller.ts" import { ArkosRequest, ArkosResponse } from "arkos"; const updateOne = async (req: ArkosRequest, res: ArkosResponse) => { const { id } = req.params; // string, validated UUID const { notify } = req.query; // boolean, coerced const { name, email } = req.body; // typed, validated }; export default { updateOne }; ``` # Deduplication and Freshness Both features key off `_meta`, a `{ mid, timestamp }` object Arkos expects on incoming payloads and injects automatically on outgoing ones (see [Enhanced Socket](/docs/guides/websockets/enhanced-socket#gotcha-outgoing-_meta-is-injected-automatically)). If you're using the [client Library](/docs/guides/websockets/advanced/frontend-integrations/overview), this is handled for you. If you're emitting from a raw `socket.io-client` connection, you need to send `_meta` yourself for these features to work. ## Deduplication [#deduplication] Protects against the same logical message being processed twice — network retries, double-clicks, at-least-once delivery from a queue in front of your sockets. ```ts gateway.on({ event: "send_message", dedup: { ttl: 600 } }, handler); ``` ### How it works [#how-it-works] 1. Deduplication is **on by default** for every event, at a Gateway-wide default of `{ enabled: true, ttl: 3600 }` (seconds). 2. Every incoming message must carry `data._meta.mid` — a unique string ID for that logical message. If it's missing, empty, or not a string, Arkos throws a `BadRequestError` before your handler ever runs. 3. Arkos atomically checks-and-sets a key `arkos::dedup:{event}:{mid}` in the configured store. If the key already exists, the message is a duplicate: your handler is skipped, and if the client passed an ack, it receives `{ success: true, duplicate: true }` — not an error, since from the sender's perspective the message did succeed the first time. 4. `_meta` is stripped from `data` before your handler runs; `mid`/`timestamp` are available on `socket.meta` instead. ### Turning it off [#turning-it-off] ```ts // per event gateway.on({ event: "cursor_move", dedup: false }, handler); // per gateway — every event skips dedup unless it opts back in const gateway = ArkosGateway({ name: "chat", dedup: false }); ``` Turn it off for high-frequency, order-independent events where a duplicate is harmless (cursor positions, typing indicators) — the check-and-set has a real cost per message. ### Config precedence [#config-precedence] `dedup` resolves event → Gateway → parent Gateway, with the event-level config taking priority and merging on top of the rest: ```ts const parent = ArkosGateway({ name: "chat", dedup: { ttl: 3600 } }); const child = ArkosGateway({ name: "rooms" }); parent.use(child); child.on({ event: "x", dedup: { ttl: 60 } }, handler); // ttl: 60 wins child.on({ event: "y" }, handler); // inherits ttl: 3600 from parent ``` ## Freshness — `maxAge` [#freshness--maxage] Rejects messages that are simply too old to matter — a client that reconnects after being offline for a while can have a backlog of queued actions; `maxAge` lets you drop the stale ones instead of processing them as if they just happened. ```ts gateway.on( { event: "cursor_move", maxAge: 5000 }, // reject anything older than 5s handler ); ``` ### How it works [#how-it-works-1] 1. If `maxAge` is set (per event, per Gateway, or inherited from a parent) and the incoming message has no `_meta.timestamp`, Arkos throws immediately — `maxAge` without a timestamp to check is a config error, not a runtime skip. 2. If `_meta.timestamp` is present, Arkos always validates it (even without `maxAge` set): an unparseable date throws `InvalidTimestamp`, and a timestamp more than 1 second in the future throws `FutureTimestamp` — a cheap clock-skew/tampering guard that applies regardless of whether you configured `maxAge`. 3. If `maxAge` is set and `Date.now() - timestamp > maxAge`, the message is rejected with `StaleMessage`. ### Config precedence [#config-precedence-1] Same resolution order as `dedup` — event overrides Gateway, Gateway overrides parent: ```ts const parent = ArkosGateway({ name: "chat", maxAge: 60_000 }); const child = ArkosGateway({ name: "rooms" }); parent.use(child); child.on({ event: "x", maxAge: 300_000 }, handler); // 5 minutes, overrides parent's 1 minute child.on({ event: "y" }, handler); // inherits 1 minute from parent ``` If neither the event, the Gateway, nor any parent sets `maxAge`, no age check runs at all — only the always-on future-timestamp guard applies (and only if `_meta.timestamp` was sent in the first place). # Stores and Scaling Rate limiting and deduplication both need somewhere to keep state. That's the `ArkosGatewayStore` — a single interface backing both features, passed once at registration: ```ts gateway.register(io, { store: myStore }); ``` ## The default store [#the-default-store] If you don't pass one, Arkos uses an in-memory store — zero config, works out of the box. ```ts export interface ArkosGatewayStore { increment( key: string, windowMs: number ): Promise<{ count: number; resetAt: number }>; clear(prefix: string): Promise; has(key: string): Promise; set(key: string, ttl: number): Promise; setIfNotExists(key: string, ttl: number): Promise; } ``` * `increment` backs rate limiting — one counter per `arkos::rl:{socketId}:{event}` key, reset every `windowMs`. * `has` / `set` / `setIfNotExists` back deduplication — keys are `arkos::dedup:{event}:{mid}`, with `setIfNotExists` doing the atomic check-and-set that makes dedup race-safe. * `clear(prefix)` is called on disconnect to drop a socket's rate-limit state (`arkos::rl:{socketId}`). ### Default store is single-instance only [#default-store-is-single-instance-only] `MemoryGatewayStore` holds everything in a `Map` in process memory. Run more than one instance of your app — multiple Node processes, multiple containers behind a load balancer — and each instance has its own view of rate limits and dedup state. A client can get rate-limited on instance A and sail through on instance B; the same message can be deduplicated on the instance that saw it first and processed again on another. Fine for local dev and single-instance deployments, not fine once you scale horizontally. ## Writing a distributed store [#writing-a-distributed-store] Implement `ArkosGatewayStore` against Redis, Valkey, or whatever your infra already runs: ```ts import { ArkosGatewayStore } from "arkos/websockets"; import Redis from "ioredis"; export class RedisArkosGatewayStore implements ArkosGatewayStore { constructor(private redis: Redis) {} async increment(key: string, windowMs: number) { const count = await this.redis.incr(key); if (count === 1) await this.redis.pexpire(key, windowMs); const ttl = await this.redis.pttl(key); return { count, resetAt: Date.now() + ttl }; } async clear(prefix: string) { const keys = await this.redis.keys(`${prefix}*`); if (keys.length) await this.redis.del(...keys); } async has(key: string) { return (await this.redis.exists(key)) === 1; } async set(key: string, ttl: number) { await this.redis.set(key, "1", "EX", ttl); } async setIfNotExists(key: string, ttl: number) { const result = await this.redis.set(key, "1", "EX", ttl, "NX"); return result === "OK"; } } ``` ```ts gateway.register(io, { store: new RedisArkosGatewayStore(redis) }); ``` `clear(prefix)` using `KEYS` is fine at the volume a per-socket rate-limit cleanup runs at; swap it for `SCAN` if you're clearing high-cardinality prefixes elsewhere. ## Multi-tier stores [#multi-tier-stores] For a fast local cache in front of a shared distributed store — memory as L1, Redis as L2 — `MultiTierArkosGatewayStore` chains stores together. Writes go to all tiers; reads check tiers in order and promote hits to faster tiers: ```ts import { MultiTierArkosGatewayStore } from "arkos/websockets"; const store = new MultiTierArkosGatewayStore([ new MemoryGatewayStore(), new RedisArkosGatewayStore(redis), ]); gateway.register(io, { store }); ``` Rate-limit `increment()` is the one exception to "check in order" — it always increments L1 first and returns that result immediately, syncing the other tiers in the background, so a rate-limit decision never waits on a network round trip. # Authentication WebSocket authentication and authorization plug directly into Arkos's existing [Built-in Authentication System](/docs/core-concepts/authentication/setup) — there's no separate auth setup for sockets. If your HTTP routes are already authenticated, you're most of the way there. ## Enabling it on a Gateway [#enabling-it-on-a-gateway] ```ts const chatGateway = ArkosGateway({ name: "/chat", authentication: true, }); ``` With `authentication: true`, every connection to this namespace runs through Arkos's auth middleware before it's accepted. On success, `socket.currentUser` is populated and the socket is joined to a `arkos::user:{id}` room automatically — this is what powers `socket.user(id)` in the [Enhanced Socket](/docs/guides/websockets/enhanced-socket) guide. On failure, the connection is rejected before your `connection` hook ever runs. ```ts chatGateway.hook("connection", (socket) => { console.log(socket.currentUser.id); // guaranteed to exist here }); ``` ### Gotcha: you still need an auth mode configured [#gotcha-you-still-need-an-auth-mode-configured] `authentication: true` on a Gateway doesn't configure authentication from scratch — it reuses whatever mode (`static` or `dynamic`) you already set under `arkos.config.ts`. If you set `authentication: true` on a Gateway without ever configuring an authentication mode for your app, Arkos throws on startup rather than silently accepting unauthenticated sockets. ### Nested Gateways inherit this [#nested-gateways-inherit-this] A child Gateway registered via `.use()` inherits its parent's `authentication` setting unless it explicitly overrides it. See [Attaching connection middleware or nesting Gateways](/docs/guides/websockets/setup#attaching-connection-middleware-or-nesting-gateways--use). ## Per-event authorization [#per-event-authorization] Authentication answers "who is this," authorization answers "can they do this." It's set per event: ```ts title="src/modules/chat/chat.policy.ts" import { ArkosPolicy } from "arkos"; const chatPolicy = ArkosPolicy("chat").rule("DeleteMessage", { resource: "message", action: "delete", rule: ["Admin", "Moderator"], }); export default chatPolicy; ``` ```ts import chatPolicy from "@/src/modules/chat/chat.policy" chatGateway.on( { event: "delete_message", authorization: chatPolicy.DeleteMessage, }, (socket, data) => { // only reached if the check passes } ); ``` `authorization` accepts the same object shape provided by `ArkosPolicy` instances. ### Gotcha: authorization requires gateway-level authentication [#gotcha-authorization-requires-gateway-level-authentication] ```ts const chatGateway = ArkosGateway({ name: "chat", authentication: false }); chatGateway.on( { event: "delete_message", authorization: chatPolicy.DeleteMessage, }, handler ); // throws immediately, at registration time: // Event "delete_message" on "chat" gateway defines authorization rules // but the gateway has authentication: false. ``` This fails fast and loud on startup rather than at runtime on the first request — if you see this error, either set `authentication: true` on the Gateway, or drop `authorization` from the event. ## Where it sits in the request lifecycle [#where-it-sits-in-the-request-lifecycle] Authorization runs after rate limiting and before validation. See the full ordering in [Full request lifecycle for one event](/docs/guides/websockets/event-handling#full-request-lifecycle-for-one-event). # Enhanced Socket Every socket your handlers receive is a plain `socket.io` `Socket`, patched with a small set of additions Arkos calls `ArkosSocket`. Everything not covered here — rooms, `socket.id`, `socket.handshake`, `socket.join()` — is untouched `socket.io`, so its docs apply as-is. ## What's already on it [#whats-already-on-it] | Property | Populated when | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `socket.currentUser` | After successful auth, if the Gateway has `authentication: true`. See [Authentication](/docs/guides/websockets/authentication). | | `socket.data` | Incoming data, if the event has a `validation` schema it is the validated and transformed data. See [Validation](/docs/guides/websockets/validation). | | `socket.meta` | `{ mid, timestamp }` extracted from the incoming payload's `_meta`, if deduplication is active for that event. | | `socket.locals` | Scratch space for passing data between pipes and your handler. Reset automatically before every event. | ## Gotcha: outgoing `_meta` is injected automatically [#gotcha-outgoing-_meta-is-injected-automatically] Every emit — `socket.emit`, `socket.emitWithAck`, `socket.to().emit()`, `socket.broadcast.emit()` — has a `_meta: { mid, timestamp }` field injected into the payload before it goes out. You don't add this yourself, and you shouldn't try to — if you inspect what actually goes over the wire and it has fields you didn't send, this is why. ```ts socket.emit("notification", { title: "New order" }); // client receives: { title: "New order", _meta: { mid: "...", timestamp: 1720000000000 } } ``` This is what powers dedup and freshness checks on the receiving end — see [Deduplication and Freshness](/docs/guides/websockets/advanced/deduplication-and-freshness). ## Targeting rooms — `socket.to()` and `socket.broadcast` [#targeting-rooms--socketto-and-socketbroadcast] Both return an enhanced `ArkosBroadcastOperator` — the standard `socket.io` operator plus a few additions: ```ts socket.to("room-101").emit("message", data); socket.broadcast.emit("announcement", data); ``` ### `.except({ user })` [#except-user-] Standard `socket.io` `.except()` takes a room or socket ID. Arkos adds a `{ user }` shorthand that excludes every active connection belonging to one or more users — handy since one user can have multiple tabs/devices connected: ```ts socket.to("room-101").except({ user: currentUserId }).emit("message", data); socket.broadcast.except({ user: [id1, id2] }).emit("announcement", data); ``` ### `.users()` [#users] Returns the unique user IDs currently in the target room(s), derived from Arkos's internal `arkos::user:{id}` room convention: ```ts const activeUserIds = await socket.to("room-123").users(); ``` ### `.volatile`, `.compress()`, `.timeout()` [#volatile-compress-timeout] Same as plain `socket.io` — `.volatile` for events that are fine to drop if the client isn't ready (cursor positions, typing indicators), `.compress(bool)`, `.timeout(ms)` before `.emitWithAck()`. ## Targeting a specific user — `socket.user(userId)` [#targeting-a-specific-user--socketuseruserid] Targets every active connection of a given user, not just the current socket: ```ts socket.user(userId).emit("notification", { title: "New order" }); const rooms = await socket.user(userId).activeRooms(); // rooms across all their tabs/devices ``` `socket.user()` returns the same `ArkosBroadcastOperator` as `.to()`/`.broadcast`, plus `activeRooms()`. ## Retrying an ack — `socket.retry()` [#retrying-an-ack--socketretry] Wraps `emitWithAck` with exponential backoff: ```ts const ack = await socket.retry(3).emitWithAck("confirm", data); // with a per-attempt timeout const ack = await socket.retry(3).timeout(5000).emitWithAck("confirm", data); // custom base delay (ms) and multiplier — default is 1000ms base, 2x multiplier const ack = await socket.retry(3, 500, 1.5).emitWithAck("confirm", data); ``` Throws if all retries are exhausted. # Event Handling An event handler is registered with `.on(eventConfig, handler)`. `eventConfig` is where most of a Gateway's behavior is configured per-event. ```ts gateway.on( { event: "send_message", validation: SendMessageSchema, ack: true, }, (socket, data, ack) => { socket.to(data.room).emit("receive_message", data); ack?.({ status: "ok" }); } ); ``` ## Event config reference [#event-config-reference] | Field | Type | Description | | --------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | `event` | `string` | The Socket.io event name to listen for. | | `validation` | Zod schema \| class-validator DTO | See [Validation](/docs/guides/websockets/validation). | | `authorization` | `{ resource, action, rule? }` | See [Authentication](/docs/guides/websockets/authentication). | | `rateLimit` | `Partial \| false` | Overrides the Gateway-level rate limit for this event. `false` disables it entirely. | | `ack` | `boolean` | When `true`, Arkos automatically calls `ack({ success: true })` after the handler finishes, unless you already called it manually. | | `disabled` | `boolean` | Registers the config but skips wiring the handler. Useful for feature-flagging an event without deleting it. | | `maxAge` | `number` (ms) | Rejects messages older than this, based on `data._meta.timestamp`. | | `dedup` | `{ enabled?, ttl? } \| false` | Per-event override of deduplication. | `maxAge` and `dedup` are covered in full in [Deduplication and Freshness](/docs/guides/websockets/advanced/deduplication-and-freshness) — in short: `dedup` protects you from a client firing the same event twice (retries, double-clicks), `maxAge` protects you from processing a message that's simply too old to matter anymore (a queued action from a client that just reconnected after five minutes offline). Both are opt-in per event and both key off `data._meta`, which the [client SDK](/docs/guides/websockets/advanced/frontend-integrations) injects automatically. ## Handler signature [#handler-signature] ```ts (socket: ArkosSocket, data: TData, ack?: (response: any) => void) => void | Promise ``` * `socket` — see [Enhanced Socket](/docs/guides/websockets/enhanced-socket) for everything it adds on top of a plain `socket.io` socket. * `data` — the payload, post-validation, with `_meta` already stripped out (it's moved to `socket.meta` instead). * `ack` — present only if the client passed a callback as the last argument. Arkos auto-wraps it so `eventConfig.ack: true` still works even if you never call it yourself. ## Ordering: pipes run before the handler [#ordering-pipes-run-before-the-handler] A pipe is middleware scoped to this Gateway's events, running after auth/rate-limit/validation but before the handler: ```ts // runs before every event handler in this gateway gateway.pipe((socket, data) => { socket.locals.enrichedUser = enrichUser(socket.currentUser); }); // runs only before "send_message" gateway.pipe({ event: "send_message" }, (socket, data) => { rateLimitPerRoom(data.room); }); gateway.on({ event: "send_message" }, (socket, data) => { console.log(socket.locals.enrichedUser); }); ``` If you call `.pipe({ event })` before the matching `.on()` exists yet, Arkos holds onto it and merges it in once you do register the event — order of declaration doesn't matter. `.pipe()` is a Gateway-composition concern, not really an event-config concern, so the mechanics (global vs. scoped, ordering with nested Gateways, how it differs from `.use()`) live in [Middlewares and Hooks](/docs/guides/websockets/middlewares-and-hooks). This page only covers that pipes run before your handler, in registration order. ## Full request lifecycle for one event [#full-request-lifecycle-for-one-event] 1. Rate limit check (unless `rateLimit: false`) 2. Authorization check (if `authorization` is set) 3. Validation (if `validation` is set) 4. Pipes (global, then event-scoped, in registration order) 5. Your handler 6. Auto-ack (if `ack: true` and you didn't call it yourself) Any error thrown at any step goes to your `error` hook, or Arkos's default error response if you don't have one. See [Middlewares and Hooks](/docs/guides/websockets/middlewares-and-hooks#hooks). # Angular Under development! Track progress or contribute at [#261](https://github.com/Uanela/arkos/issues/261). Angular's DI system makes this structurally different from the other bindings — the starting point in the [contributing guide](https://github.com/Uanela/arkos/blob/feat/websockets-client/websockets-client/CONTRIBUTING_FRAMEWORK_BINDIND.md#angular) is more of a sketch than a final design. It needs an Angular user to shape it before it lands here as real docs. # Overview Everything below is shipped as `@arkosjs/websockets-client` and framework-specific packages on top of it. The core client handles what a raw `socket.io-client` connection doesn't: automatic `_meta` injection matching what the server expects (see [Deduplication and Freshness](/docs/guides/websockets/advanced/deduplication-and-freshness)), ack/retry/timeout ergonomics, and reactive connection status. These packages are pre-1.0 and still maturing. The core client and the React binding are used and tested; the rest are community-contributed starting points that haven't been validated in a real app yet. | Package | Status | | ------------------------------------------------------------------------------- | -------------------------------- | | [Vanilla JS](/docs/guides/websockets/advanced/frontend-integrations/vanilla-js) | Stable core API | | [React](/docs/guides/websockets/advanced/frontend-integrations/react) | Reference implementation, tested | | [Vue](/docs/guides/websockets/advanced/frontend-integrations/vue) | Under development | | [Svelte](/docs/guides/websockets/advanced/frontend-integrations/svelte) | Under development | | [Solid](/docs/guides/websockets/advanced/frontend-integrations/solid) | Under development | | [Angular](/docs/guides/websockets/advanced/frontend-integrations/angular) | Under development | Every framework package is a thin adapter over the same core client — the reactivity primitives change, the API underneath doesn't. If your framework isn't listed as stable yet, the [Vanilla JS](/docs/guides/websockets/advanced/frontend-integrations/vanilla-js) page is your building block, and the [contributing guide](https://github.com/Uanela/arkos/blob/feat/websockets-client/websockets-client/CONTRIBUTING_FRAMEWORK_BINDIND.md) has a starting-point implementation plus an open issue for each framework if you want to help ship it. # React `@arkosjs/react-websockets` is the reference framework binding for `@arkosjs/websockets-client` — the one that's actually been used and tested. If you're building a binding for another framework, this is the API shape to match as closely as your framework's idioms allow. ## Setup [#setup] ```tsx import { Manager } from "socket.io-client"; import { ArkosSocketProvider } from "@arkosjs/react-websockets"; const manager = new Manager("http://localhost:3000", { auth: { token: "your-auth-token" }, }); function App() { return ( ); } ``` One provider at the root — every `useGateway()` call below it shares the same underlying `WebsocketClient`. ## `useGateway` [#usegateway] One hook per namespace: ```tsx function Chat() { const chat = useGateway("/chat"); useEffect(() => { return chat.on("receive_message", (data) => { setMessages((m) => [...m, data]); }); }, [chat]); return
{chat.status}
; } ``` * `chat.on(event, handler)` — subscribes, cleans up automatically on unmount. * `chat.status` — reactive: `"connected" | "reconnecting" | "disconnected"`. * `chat.user` — reactive: the user object populated when the server emits `"authenticated"`, or `null`. ## `useEmit` [#useemit] For events you emit from user actions, with loading/error state handled for you: ```tsx function MessageInput({ room }: { room: string }) { const chat = useGateway("/chat"); const sendMessage = chat.useEmit("send_message"); return ( ); } ``` * `sendMessage.emit(data, options?)` — `{ ack: true }` waits for server acknowledgement; omit it for fire-and-forget. * `sendMessage.loading` — reactive, `true` while an acked emit is in flight. * `sendMessage.error` — reactive, set if the emit fails or the server returns `{ success: false }`. * `sendMessage.lastEmittedAt` — reactive timestamp of the last emit call. * `sendMessage.reset()` — clears `error`/`loading` manually. ## Cleanup [#cleanup] Handled for you — `ArkosSocketProvider` destroys the underlying client when it unmounts, and every `chat.on()` subscription cleans up when its component unmounts. # Solid Under development! Track progress or contribute at [#259](https://github.com/Uanela/arkos/issues/259). A starting-point implementation exists in the [contributing guide](https://github.com/Uanela/arkos/blob/feat/websockets-client/websockets-client/CONTRIBUTING_FRAMEWORK_BINDIND.md#solid) but hasn't been tested in a real Solid app yet — it needs a Solid user to validate it before it lands here as real docs. # Svelte Under development! Track progress or contribute at [#258](https://github.com/Uanela/arkos/issues/258). A starting-point implementation exists in the [contributing guide](https://github.com/Uanela/arkos/blob/feat/websockets-client/websockets-client/CONTRIBUTING_FRAMEWORK_BINDIND.md#svelte) but hasn't been tested in a real Svelte 5 app yet — it needs a Svelte user to validate it before it lands here as real docs. # Vanilla JS `@arkosjs/websockets-client` wraps a `socket.io-client` `Manager`. This is the layer every framework binding sits on top of — reach for this directly if you're not using one of the framework packages, or if you're building your own binding. ## Setup [#setup] ```ts import { Manager } from "socket.io-client"; import { createWebsocketClient } from "@arkosjs/websockets-client"; const manager = new Manager("http://localhost:3000", { auth: { token: "your-auth-token" }, reconnection: true, }); const client = createWebsocketClient(manager); ``` ## Getting a Gateway client [#getting-a-gateway-client] One `GatewayClient` per namespace, matching the `name` you gave `ArkosGateway` on the server: ```ts const gateway = client.gateway("/chat"); ``` ## Listening [#listening] ```ts const off = gateway.on("receive_message", (data) => { console.log(data); }); off(); // cleanup ``` ## Emitting [#emitting] ```ts // fire and forget gateway.emit("send_message", { room: "general", content: "hello" }); // with ack const result = await gateway.emit("send_message", data, { ack: true, timeout: 5000, retries: 3, }); // result: { success, data, error } ``` `_meta.mid` and `_meta.timestamp` are injected on every emit automatically — this is what makes [deduplication and freshness checks](/docs/guides/websockets/advanced/deduplication-and-freshness) work without you touching them. ## Reacting to connection state [#reacting-to-connection-state] ```ts const unsub = gateway.subscribe({ onStatus: (status) => { // "connected" | "disconnected" | "reconnecting" | "connecting" }, onUser: (user) => { // populated when the server emits "authenticated" }, }); unsub(); ``` Non-reactive, synchronous reads are also available directly: ```ts gateway.status; gateway.user; ``` ## Cleanup [#cleanup] ```ts client.destroy(); ``` Framework bindings wrap this in your framework's lifecycle/reactivity primitives so you don't manage `off()`/`unsub()` calls by hand — see [React](/docs/guides/websockets/advanced/frontend-integrations/react) for the shipped example. # Vue Under development! Track progress or contribute at [#260](https://github.com/Uanela/arkos/issues/260). A starting-point implementation exists in the [contributing guide](https://github.com/Uanela/arkos/blob/feat/websockets-client/websockets-client/CONTRIBUTING_FRAMEWORK_BINDIND.md#vue) but hasn't been tested in a real Vue app yet — it needs a Vue 3 user to validate it before it lands here as real docs. # Middlewares and Hooks Arkos Gateways give you three distinct extension points, and it's easy to reach for the right one: | API | Runs when | Scope | | --------- | ------------------------------------------------------------------------ | ----------------------------- | | `.use()` | On connection, before a socket is accepted — or composes a child Gateway | Connection-level | | `.pipe()` | Before an event handler runs | Event-level, global or scoped | | `.hook()` | On `connection`, `disconnect`, or `error` | Lifecycle-level | ## `.use()` — connection middleware and Gateway composition [#use--connection-middleware-and-gateway-composition] This is plain `socket.io` connection middleware — same `(socket, next)` signature you already know: ```ts chatGateway.use((socket, next) => { if (isBanned(socket.handshake.address)) return next(new Error("banned")); next(); }); ``` `.use()` also accepts a Gateway instance, which nests it as a child: ```ts chatGateway.use(adminGateway); ``` Nesting is a component-composition concern — it's the same idea as composing Routers, just for Gateways. The mechanics (namespace prefixing, what config inherits) are covered in [Setup](/docs/guides/websockets/setup#attaching-connection-middleware-or-nesting-gateways--use); this page only covers that `.use()` is the API for it. ## `.pipe()` — event middleware [#pipe--event-middleware] A pipe runs after auth/rate-limit/validation, before the handler. Register it globally (runs before every event in this Gateway) or scoped to one event: ```ts // global — every event in this gateway chatGateway.pipe((socket, data) => { socket.locals.startedAt = Date.now(); }); // scoped — only "send_message" chatGateway.pipe({ event: "send_message" }, (socket, data) => { assertNotMuted(socket.currentUser, data.room); }); ``` Declaration order does matter — if you scope a pipe to an event that isn't registered yet, Arkos holds onto it and merges it in once `.on()` is called for that event. ```ts chatGateway.pipe({ event: "future_event" }, myPipe); // fine, registered first chatGateway.on({ event: "future_event" }, handler); // myPipe still runs ``` `.pipe()` is per-Gateway — a child Gateway does not automatically inherit its parent's pipes. If you need shared logic across a parent and its children, put it in a `.use()` connection middleware instead, which does apply before namespace-level auth on every socket in that branch. ## `.hook()` — lifecycle hooks [#hook--lifecycle-hooks] ```ts chatGateway.hook("connection", (socket) => { console.log("connected", socket.currentUser?.id); }); chatGateway.hook("disconnect", (socket) => { console.log("disconnected", socket.id); }); chatGateway.hook("error", (err, socket) => { socket.emit("error", { message: err.message }); }); ``` * **`connection`** — runs once, after authentication succeeds (if enabled), before any event listeners are wired for that socket. Throwing here prevents event listeners from being registered at all for that socket. * **`disconnect`** — runs on socket disconnect, after Arkos clears that socket's rate-limit state. * **`error`** — runs whenever anything in the event lifecycle throws (validation, authorization, rate limit, dedup, or your own handler). ### The `error` hook's contract [#the-error-hooks-contract] If your `error` hook calls `socket.emit(...)` itself, Arkos considers the error handled and stops there. If it doesn't emit anything (or throws itself), Arkos falls back to its own default error response — a detailed payload in development, a generic message in production. ```ts chatGateway.hook("error", (err, socket) => { // you own the response — Arkos won't also send one socket.emit("error", { message: err.message, code: err.code }); }); ``` If you don't register an `error` hook at all, Arkos always sends its own default response. Nested Gateways inherit all of their parent's hooks, in addition to any of their own. # Setup > Available from 1.7.0-rc Arkos WebSockets are built directly on top of `socket.io`. If you already know `socket.io`, most of what follows is familiar — Arkos adds structure (validation, auth, dedup, rate limiting) around it, it doesn't replace it. ## Creating a Gateway [#creating-a-gateway] ```ts import { ArkosGateway } from "arkos/websockets"; const gateway = ArkosGateway({ name: "/", authentication: true, }); ``` `config.name` becomes the Socket.io namespace. It defaults to `"/"` if omitted. ## Registering it [#registering-it] ```ts import arkos from "arkos"; import { Server } from "socket.io"; import http from "node:http"; import gateway from "@/src/gateway"; const app = arkos() await app.listen() const server = http.createServer(app) const io = new Server(httpServer); gateway.register(io); app.listen(server) ``` `register(io, options?)` can only be called once per `io` instance — call it on your root Gateway and compose the rest with `.use()` (see below). Calling it twice throws. ```ts gateway.register(io, { store: myRedisStore, // optional — defaults to an in-memory store }); ``` The `store` option backs both rate limiting and deduplication. See [Stores and Scaling](/docs/guides/websockets/advanced/stores-and-scaling) if you're running more than one instance. ## Attaching connection middleware or nesting Gateways — `.use()` [#attaching-connection-middleware-or-nesting-gateways--use] `.use()` accepts either a raw Socket.io connection middleware, or another Gateway to nest under this one: ```ts gateway.use((socket, next) => { console.log("incoming connection", socket.id); next(); }); // nested gateway — inherits this gateway's name as a namespace prefix, // plus its authentication and rateLimit config gateway.use(adminGateway); ``` A nested Gateway's namespace becomes `${parent.name}/${child.name}`. It inherits the parent's `authentication` and `rateLimit` unless it sets its own, and it inherits all of the parent's lifecycle hooks. It does **not** automatically inherit the parent's `.pipe()` middlewares — those are scoped to the Gateway that registered them. ## Attaching an event handler — `.on()` [#attaching-an-event-handler--on] ```ts gateway.on({ event: "send_message" }, (socket, data) => { socket.to(data.room).emit("receive_message", data); }); ``` This is the shallow version — `.on()` takes a full event config (`validation`, `authorization`, `rateLimit`, `ack`, `dedup`, `maxAge`, `disabled`). The full breakdown, including how it interacts with `.pipe()`, is in [Handling Events](/docs/guides/websockets/event-handling). ## Minimal end-to-end example [#minimal-end-to-end-example] ```ts title="src/gateway.ts" import { ArkosGateway } from "arkos/websockets"; const gateway = ArkosGateway({ name: "/" }); gateway.on({ event: "send_message" }, (socket, data) => { socket.to(data.room).emit("receive_message", data); }); export default gateway; ``` ```ts title="src/server.ts" import { Server } from "socket.io"; import http from "http"; import app from "@/src/app"; import gateway from "@/src/gateway"; await app.build(); const server = http.createServer(app); const io = new Server(server); gateway.register(io); app.listen(server); ``` Next: [Handling Events](/docs/guides/websockets/event-handling) for the full `.on()` config, or [Authentication](/docs/guides/websockets/authentication) if your Gateway needs `socket.currentUser`. # Validation Event validation reuses the exact same validation resolver you already configured for your HTTP routes — Zod or class-validator, whichever you set in `arkos.config.ts`. There's no separate WebSocket validation system. ```ts chatGateway.on( { event: "send_message", validation: SendMessageSchema }, (socket, data) => { // data is typed and validated against SendMessageSchema } ); ``` ## What happens under the hood [#what-happens-under-the-hood] 1. Arkos checks the validator you passed is valid for your configured resolver. If your app is set to `zod` and you pass a class-validator DTO, this throws immediately with a clear message rather than failing silently at runtime. 2. Your validator can signal one of three outcomes via `shouldValidate`: * **Validate normally** — the default. Data is parsed/validated and the result replaces `data`. * **`"passthrough"`** — validation is skipped entirely for this message; `data` is passed through as-is. * **`"prohibit"`** — the event data is rejected outright with a `BadRequestError`. Useful for events that shouldn't carry a payload at all. 3. On failure, the error is run through the same error-prettifier used for HTTP requests, so client-facing validation errors look consistent whether they came from a REST call or a socket event. ## Errors [#errors] A failed validation throws a `BadRequestError` with code `InvalidData`, caught by the same error pipeline as everything else in the event lifecycle — see [Middlewares and Hooks](/docs/guides/websockets/middlewares-and-hooks#hooks) for how `error` hooks intercept it, or [Handling Events](/docs/guides/websockets/event-handling#full-request-lifecycle-for-one-event) for where validation sits in the request lifecycle. ## `_meta` is not part of your schema [#_meta-is-not-part-of-your-schema] Every payload from the [client SDK](/docs/guides/websockets/frontend-integrations/overview) carries a `_meta: { mid, timestamp }` field. Arkos strips it out and moves it to `socket.meta` before your validator ever sees the payload — you don't need to account for it in your schema. # Introduction **Arkos.js** is a modern JavaScript/TypeScript framework for quickly building secure and scalable [**Node.js**](https://nodejs.org) server-side [**RESTful**](https://restfulapi.net) applications. It uses progressive JavaScript (just like [**NestJS**](https://nestjs.com)), backed with and has full support for [**TypeScript**](https://typescriptlang.org) (but still allows developers to code using vanilla JavaScript). It combines the world's 2 most used programming paradigms FP (Functional Programming) and OOP (Object Oriented Programming). Behind the scenes, Arkos uses the undisputed and most used JavaScript Server-Side Framework [**Express**](https://expressjs.com) altogether with most modern and type-safe ORM [**Prisma**](https://prisma.io) for database integration. Arkos introduces a new way of developing RESTful APIs in JavaScript world, by combining Node.js most used framework (Express), Prisma ORM for database management and the modular architecture from NestJS, for creating a level of abstraction of well established and standardized RESTful APIs principles and offering many tools out of the box just like like [**Django**](https://djangoproject.com) in Python and [**Laravel**](https://laravel.com) in PHP. Even though Arkos creates a level of abstraction on top of Express and Prisma, it still allows developers to fully access their APIs directly and write plain Express or Prisma code as needed. This gives developers the ability to leverage all of what Arkos offers out of the box while still being able to do everything they can do with pure Express and Prisma. ## Philosophy [#philosophy] For long years, thanks to [**Ryan Dahl**](https://github.com/ry) building Node.js, JavaScript has become almost the default language on the web both for Client-Side and Server-Side Applications. Then we watched Client-Side frameworks like [**Angular**](https://angular.dev), [**React**](https://react.dev), [**Vue**](https://vuejs.org) and others, and also the rise of JavaScript Server-Side frameworks such as Express and [**Fastify**](https://fastify.io) all minimalistics. Time went by and we saw the rise of frameworks just like NestJS and [**AdonisJS**](https://adonisjs.com) trying to improve the experience of the development of JavaScript Server-Side applications. Although both succeeded very well on their own focused niches Nest with strong OOP and Angular like experience and Adonis with the Fullstack MVC vision just like PHP, **Arkos.js** introduces a different proposal for RESTful APIs by **Safeguarding ExpressJS Paradigms** and **Enhancing Developer Experience** by providing a lot of common tools that developers have been creating from scratch, simply out of the box and letting them to still write any type of code they write when using Express, despite Arkos.js tools and focus on RESTful APIs. ## Why Prisma ORM [#why-prisma-orm] On the past [**Uanela Como**](https://github.com/uanela) *(The creator)* initially favored Mongoose ORM and became skeptical when first encountering Prisma's different approach. However, deeper exploration revealed Prisma's key strengths: superior separation of concerns, type safety, and intuitive querying that focuses on what an ORM should excel at. Rather than building a custom ORM, Arkos.js strategically integrates with Prisma - one of the most modern and widely adopted ORMs in JavaScript. Combined with Arkos.js's BaseService Class, this creates what Uanela describes as "an unmatched Prisma integration" that enhances established tools while adding enterprise-level patterns for RESTful API development. ## Key Features Overview [#key-features-overview] **1. Automatic RESTful Endpoints Generation** - Generate complete CRUD operations for Prisma models with zero boilerplate code and full customize the whole flow using interceptor middlewares. **2. Built-in Authentication System** - JWT-based auth with user management, password hashing, and role-based access control. **3. Enterprise-Grade Middlewares** - Security headers, CORS, compression, request parsing, and error handling pre-configured. **4. Type-Safe Data Validation** - Automatic request/response validation using [**Class-Validator**](https://www.npmjs.com/package/class-validator) or [ **Zod** ](https://zod.dev) with TypeScript and JavaScript integration. **5. Advanced File Management** - Easy file uploads with different file type processing and image optimization. **6. Email Service Tool** - Quickly send emails with minimal configuration, using our favorite Node.js mailing tool [**Nodemailer**](https://www.npmjs.com/package/nodemailer). **7. Performance Optimization** - Built-in caching, rate limiting, and database query optimization with prisma. **8. Developer Experience** - Auto-generated Swagger docs, structured logging, hot reload, and comprehensive CLI tools for scaffolding and creating components like controllers, routers, services and many more. ## Pre-requisite Knowledge [#pre-requisite-knowledge] Our documentation assumes some familiarity with some web development tools, mainly those that are the core of Arkos.js. Before getting started, it'll help if you're comfortable with: * JavaScript or TypeScript * Express * Prisma ORM If you are new to Express, Prisma ORM or JavaScript backend development at all or needs a refresh, we highly recommend getting started with some of the listed courses below: * [**Node.js and Express.js - Full Course**](https://youtu.be/Oe421EPjeBE?si=QRPJxuNRWrMo0BN9) from freeCodeCamp.org * [**NodeJS ExpressJS PostgreSQL Prisma Course**](https://youtu.be/9BD9eK9VqXA?si=HqVVRj11iYBET_Ow) from Smoljames * [**JavaScript Tutorial Full Course**](https://youtu.be/EerdGm-ehJQ?si=k5LmjF8nSDQ-jUtc) from SuperSimpleDev ## Next Steps [#next-steps] * [Quick Start](/docs/quick-start) - Create an Hello World Arkos app * [Getting Started](/docs/getting-started/installation) - Set up your first Arkos project * [Project Structure](/docs/getting-started/project-structure) - See the **Arkos** project structure > The name "Arkos" comes from the Greek word "ἀρχή" (Arkhē), meaning "beginning" or "foundation". This reflects our goal of providing a solid foundation for backend development. # What's Next Arkos is already being used in production and we will old the `beta version` label because we're still hearing from community up until the `v2.0` which is already under development already. This page covers what landed in the v2.0 pre-release track and what is still planned. ## v2.0 — In Progress [#v20--in-progress] v2.0 is a significant architectural shift. The core theme is **explicitness over file-based magic** — instead of Arkos scanning your file system for `*.interceptors.ts`, `*.hooks.ts`, `*.auth.ts`, and `*.query.ts` files, you register everything explicitly via `app.load()`. ### Explicit Registration via `app.load()` [#explicit-registration-via-appload] The dynamic loader is gone. Arkos no longer discovers components by scanning your module directories. Instead you register route hooks and service hooks directly: ```ts title="src/loadables.ts" import { ArkosRouteHook, ArkosServiceHook } from "arkos"; import app from "./app"; const userRouteHook = ArkosRouteHook("user") .createOne({ authentication: true }) .findMany({ authentication: false }); const userServiceHook = ArkosServiceHook("user") .createOne({ before: [async ({ data }) => { data.slug = data.title.toLowerCase().replace(/\s+/g, "-"); }], }); app.load(userRouteHook, userServiceHook); ``` ```ts title="src/app.ts" import arkos from "arkos"; import "./loadables"; const app = arkos(); app.listen(); ``` `app.load()` must be called before `app.build()` or `app.listen()`. ### `ArkosRouteHook` — Fluent Route Configuration [#arkosroutehook--fluent-route-configuration] Replaces the `export const hook: RouteHook` file export and the `*.interceptors.ts` file convention. Each operation method accepts the same route config options (authentication, validation, rate limiting) plus `before`, `after`, and `onError` lifecycle arrays — all in one place: ```ts const postRouteHook = ArkosRouteHook("post") .findMany({ authentication: false }) .createOne({ authentication: postPolicy.Create, validation: { body: CreatePostSchema }, before: [setAuthor], after: [notifyFollowers], onError: [cleanupImage], }) .deleteOne({ authentication: postPolicy.Delete }); ``` Available for `"auth"` and `"file-upload"` modules too, with operation methods narrowed to what's relevant for each. ### `ArkosServiceHook` — Fluent Service Hook Configuration [#arkosservicehook--fluent-service-hook-configuration] Replaces the `*.hooks.ts` file convention. Typed against your Prisma model's actual operation args: ```ts const postServiceHook = ArkosServiceHook("post") .createOne({ before: [async ({ data }) => { data.slug = slugify(data.title); }], after: [async ({ result }) => { await index(result); }], onError: [async ({ error }) => { logger.error(error); }], }); ``` ### `app.build()` is Now Synchronous [#appbuild-is-now-synchronous] Previously async, `app.build()` is now synchronous. If you need a custom HTTP server for WebSockets or similar, the pattern is: ```ts title="src/app.ts" import arkos from "arkos"; import http from "http"; import arkosConfig from "./arkos.config"; const app = arkos(); app.build(); const server = http.createServer(app); app.listen(server); ``` ### PrismaClient Now Passed Explicitly [#prismaclient-now-passed-explicitly] Arkos no longer instantiates Prisma internally. You pass your own instance in the config: ```ts title="arkos.config.ts" import { defineConfig } from "arkos/config"; import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); export default defineConfig({ prisma: { instance: prisma }, }); ``` ### Dynamic Query Filter Parameters in OpenAPI [#dynamic-query-filter-parameters-in-openapi] The OpenAPI docs for `findMany`, `updateMany`, and `deleteMany` now generate real query filter parameters from your Prisma model fields instead of a generic `filters` string. String fields show `icontains`, numeric fields show `equals`/`gte`/`lte`, DateTime fields follow the same pattern, booleans and enums are handled correctly, and single relations show a filter keyed by the reference field. ### Removed in v2.0 [#removed-in-v20] | Removed | Replacement | | ---------------------------------------------- | ------------------------------------------- | | `*.interceptors.ts` file convention | `ArkosRouteHook` `before`/`after`/`onError` | | `*.hooks.ts` file convention | `ArkosServiceHook` | | `*.auth.ts` file convention | `ArkosPolicy` via `ArkosRouteHook` | | `*.query.ts` / query options files | `prismaArgs` on `ArkosRouteHook` | | `export const hook: RouteHook` in router files | `ArkosRouteHook` via `app.load()` | | `arkos generate interceptors` | `arkos generate route-hook` | | `arkos generate hooks` | `arkos generate service-hook` | | `arkos generate auth-configs` | `arkos generate policy` | | `arkos generate query-options` | `prismaArgs` on `ArkosRouteHook` | | `swagger.mode` config | `validation.resolver` | | Dynamic loader file scanning | `app.load()` explicit registration | ## Planned [#planned] * AWS S3 built-in file upload support * Video DASH and HLS built-in processing * Aggregation queries through auto-generated endpoints * ORM support beyond Prisma — Mongoose is high on the list ## Share Your Ideas [#share-your-ideas] Open an issue on [GitHub](https://github.com/uanela/arkos/issues) — the roadmap is shaped by the community. # Quick Start This is a quick start guide to get something running fast. For a more robust project setup with TypeScript, Prisma, and full configuration, see the [Installation Guide](/docs/getting-started/installation). Let's build your first Arkos application. You'll create a simple API server with one endpoint that returns "Hello World". ## Installation [#installation] Create a new directory and install Arkos: ```bash mkdir my-arkos-app cd my-arkos-app pnpm init ``` ```bash pnpm add arkos pnpm add -D tsx tsx-strict ``` ```bash npm install arkos npm add -D tsx tsx-strict ``` ## Hello World [#hello-world] Create `src/router.ts`: ```ts title="src/router.ts" import { ArkosRouter } from "arkos"; const router = ArkosRouter(); router.get({ path: "/" }, (req, res) => { res.json({ message: "Hello World" }); }); export default router; ``` Then create `src/app.ts`: ```ts title="src/app.ts" import arkos from "arkos"; import router from "./router"; const app = arkos(); app.use(router); app.listen(); ``` Create `src/router.ts`: ```ts title="src/router.ts" import { ArkosRouter } from "arkos"; const router = ArkosRouter(); router.get({ path: "/" }, (req, res) => { res.json({ message: "Hello World" }); }); export default router; ``` Then create `src/app.ts`: ```ts title="src/app.ts" import arkos from "arkos"; import router from "./router"; arkos.init({ use: [router], }); ``` Create `src/router.ts`: ```ts title="src/router.ts" import { Router } from "express"; const router = Router(); router.get("/", (req, res) => { res.json({ message: "Hello World" }); }); export default router; ``` Then create `src/app.ts`: ```ts title="src/app.ts" import arkos from "arkos"; import router from "./router"; arkos.init({ routers: { additional: [router], }, }); ``` ## Run It [#run-it] ```bash npx arkos dev ``` Open `http://localhost:8000` — you'll see: ```json { "message": "Hello World" } ``` This small app is already running with compression, rate limiting, CORS, JSON body parsing, cookie parsing, query parsing, request logging, and security headers — all on by default. Every one of them is configurable or replaceable through `arkos.config.ts`. See [Global Middlewares](/docs/guides/global-middlewares) for the full list and options. ## Add Another Route [#add-another-route] ```ts title="src/router.ts" import { ArkosRouter } from "arkos"; const router = ArkosRouter(); router.get({ path: "/" }, (req, res) => { res.json({ message: "Hello World" }); }); router.get({ path: "/ping" }, (req, res) => { res.json({ pong: true }); }); export default router; ``` ```ts title="src/router.ts" import { ArkosRouter } from "arkos"; const router = ArkosRouter(); router.get({ path: "/" }, (req, res) => { res.json({ message: "Hello World" }); }); router.get({ path: "/ping" }, (req, res) => { res.json({ pong: true }); }); export default router; ``` ```ts title="src/router.ts" import { Router } from "express"; const router = Router(); router.get("/", (req, res) => { res.json({ message: "Hello World" }); }); router.get("/ping", (req, res) => { res.json({ pong: true }); }); export default router; ``` The `src/app.ts` file stays the same — just add more routes to `src/router.ts` as your app grows. ## What's Next [#whats-next] This was the minimum. A real Arkos app also auto-generates full CRUD endpoints from your Prisma models, has a built-in authentication system, request validation, file uploads, and a lot more — all with zero boilerplate. The [Installation Guide](/docs/getting-started/installation) walks through setting all of that up properly with TypeScript, Prisma, and a solid project structure. That's the right next step before building anything serious. * **[Installation](/docs/getting-started/installation)** — Complete setup with TypeScript, Prisma, and configuration * **[Project Structure](/docs/getting-started/project-structure)** — Organize your code as it grows * **[Routing Concepts](/docs/core-concepts/routing/concepts)** — Custom routes vs auto-generated routes # App Error Guide `AppError` is a specialized error class in the `Arkos` designed to standardize error handling across your application. It extends JavaScript's native `Error` class and adds properties that make it suitable for API and web application error management. ## Purpose [#purpose] The `AppError` class serves several important purposes: 1. **Standardized Error Format**: Creates a consistent error structure throughout your application 2. **HTTP Integration**: Maps errors directly to appropriate HTTP status codes 3. **Operational vs Programming Errors**: Distinguishes between operational errors (expected problems like invalid input) and programming errors (bugs) 4. **Rich Error Information**: Provides context through metadata and error codes 5. **Client-Friendly Responses**: Facilitates creating meaningful error responses for API clients ## Class Properties [#class-properties] | Property | Type | Description | | --------------- | -------------------------------- | -------------------------------------------------------------------------- | | `message` | `string` | Human-readable error description | | `statusCode` | `number` | HTTP status code (e.g., 400, 404, 500) | | `status` | `string` | Status type derived from status code (`"fail"` for 4xx, `"error"` for 5xx) | | `isOperational` | `boolean` | Indicates if error is operational (expected) vs programming error | | `code` | `string` (optional) | Custom error code for categorization and client reference | | `meta` | `Record` (optional) | Additional contextual information about the error | | `missing` | `boolean` | Flag to indicate if a resource is missing (defaults to `false`) | ## Usage Examples [#usage-examples] ### Basic Usage [#basic-usage] ```typescript import { AppError } from "arkos/error-handler"; // In a route handler or service if (!userId) { throw new AppError("User ID is required", 400); } ``` ### With Error Code and Metadata [#with-error-code-and-metadata] ```typescript import { AppError } from "arkos/error-handler"; // Providing additional context throw new AppError( "User not found", 404, { userId: requestedId, requestTime: new Date() }, "USER_NOT_FOUND" ); ``` :::tip Hint If throwing an error while processing a request you may want to wrap your async or even normal function (that throws an error) inside `catchAsync` ([read more about](/docs/reference/catch-async)), so that you can harness the `Built-in Error Handler` ([read more about](/docs/guides/error-handling/overview)). ::: ### With Async Error Handling [#with-async-error-handling] ```typescript import { AppError, catchAsync } from "arkos/error-handler"; import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; export const getUserById = catchAsync( async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const user = await userService.findById(req.params.id); if (!user) { throw new AppError( "User not found", 404, { userId: req.params.id }, "USER_NOT_FOUND" ); } res.status(200).json({ status: "success", data: { user }, }); } ); ``` You can read more about the `catchAsync` function [here](/docs/reference/catch-async). ## Error Handling Workflow [#error-handling-workflow] 1. **Throw AppError instances** in your controllers, services, or middleware 2. Use the `catchAsync` utility for async functions to automatically catch and forward errors 3. Implement a global error handler middleware that processes `AppError` instances 4. The error handler can distinguish between operational errors (`isOperational: true`) and programming errors ## Why Use AppError? [#why-use-apperror] 1. **Consistency**: Standardizes error handling across your entire application 2. **Readability**: Makes error causes clearer in logs and debugging 3. **Security**: Helps prevent leaking sensitive error details to clients 4. **Client Experience**: Enables generating user-friendly error messages 5. **Maintenance**: Makes error patterns easier to identify and fix 6. **API Design**: Follows REST best practices for error responses ## Best Practices [#best-practices] 1. **Be Specific**: Use descriptive error messages that help identify the issue 2. **Use Proper Status Codes**: Match HTTP semantics (400 for bad requests, 404 for not found, etc.) 3. **Include Context**: Add relevant data in the `meta` object for debugging 4. **Consistent Codes**: Establish a system for your error codes (e.g., `RESOURCE_OPERATION_ISSUE`) 5. **Set Operational Flag**: Only set `isOperational: true` for expected errors (by default). # Arkos Configuration Arkos provides a comprehensive configuration system that allows you to customize every aspect of your application. This reference covers all available configuration options for both `arkos.init()` and `arkos.config.ts`. The dedicated configuration file was introduced on `v1.4.0-beta` it was made for the clearly separate concerns between what is really application configuration and what is initialization configuration. And this also makes possible for different tools such as the [**Built-in CLI**](/docs/tooling/cli/overview) to make usage of the confiugration when generating different components in your project. ### Key Changes From `v1.4.0-beta` [#key-changes-from-v140-beta] * **Split Configuration**: Configuration is now split between `arkos.init()` (app initialization) and `arkos.config.ts` (static configuration) * **Simplified Middleware Configuration**: Individual middleware options replace complex `middlewares` object * **Unified Router Registration**: All custom routers use the `use` array * **Enhanced ArkosRouter**: New declarative configuration for routes ### File Structure Changes [#file-structure-changes] **Two-file setup:** ```typescript // src/app.ts - App initialization import arkos from "arkos"; import customRouter from "./routers/custom.router"; arkos.init({ use: [customRouter], configureApp: (app) => { app.set("trust proxy", 1); }, }); ``` ```typescript // arkos.config.ts - Static configuration import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { port: 3000, authentication: { enabled: true, mode: "static", }, validation: { resolver: "zod", }, }; export default arkosConfig; ``` **Single-file setup:** ```typescript // src/app.ts - Everything in one file import arkos from "arkos"; import customRouter from "./routers/custom.router"; arkos.init({ port: 3000, authentication: { mode: "static", }, validation: { resolver: "zod", }, routers: { additional: [customRouter], }, configureApp: (app) => { app.set("trust proxy", 1); }, }); ``` ## Configuration Structure [#configuration-structure] ### ArkosInitConfig (arkos.init()) [#arkosinitconfig-arkosinit] Used for app initialization and runtime configuration: ```typescript interface ArkosInitConfig { use?: ( | IArkosRouter | express.Router | ArkosRequestHandler | ArkosErrorRequestHandler )[]; configureApp?: (app: express.Express) => Promise | any; configureServer?: (server: http.Server) => Promise | any; } ``` ### ArkosConfig (arkos.config.ts) [#arkosconfig-arkosconfigts] Used for static application configuration: ```typescript interface ArkosConfig { // Basic settings welcomeMessage?: string; port?: number; host?: string; // Feature configurations authentication?: AuthenticationConfig; validation?: ValidationConfig; fileUpload?: FileUploadConfig; middlewares?: MiddlewareConfig; routers?: RouterConfig; email?: EmailConfig; swagger?: SwaggerConfig; request?: RequestConfig; debugging?: DebuggingConfig; } ``` ## Configuration Properties [#configuration-properties] ### Basic Application Settings [#basic-application-settings] ```typescript // arkos.config.ts const arkosConfig: ArkosConfig = { welcomeMessage: "Welcome to Our API", port: 3000, host: "0.0.0.0", }; export default arkosConfig; ``` ```typescript // src/app.ts arkos.init({ welcomeMessage: "Welcome to Our API", port: 3000, host: "0.0.0.0", }); ``` #### `welcomeMessage` [#welcomemessage] * **Type**: `string` * **Default**: `"Welcome to our Rest API generated by Arkos, find more about Arkos at www.arkosjs.com."` * **Description**: Message returned when accessing `GET /api` #### `port` [#port] * **Type**: `number` * **Default**: `8000` or `process.env.PORT` or `-p` argument * **Description**: Port where the application will run #### `host` [#host] * **Type**: `string` * **Default**: `localhost` * **Description**: Host to bind the server to ### Authentication Configuration [#authentication-configuration] ```typescript // arkos.config.ts const arkosConfig: ArkosConfig = { authentication: { enabled: true, mode: "static", login: { allowedUsernames: ["email", "username"], sendAccessTokenThrough: "both", }, rateLimit: { windowMs: 5000, limit: 10, }, jwt: { secret: process.env.JWT_SECRET, expiresIn: "7d", cookie: { secure: process.env.NODE_ENV === "production", httpOnly: true, sameSite: "lax", }, }, }, }; export default arkosConfig; ``` ```typescript // src/app.ts arkos.init({ authentication: { mode: "static", login: { allowedUsernames: ["email", "username"], sendAccessTokenThrough: "both", }, // ... other auth config }, }); ``` #### `authentication.enabled` [#authenticationenabled] * **Type**: `boolean` * **Default**: `true` * **Description**: Completely disable authentication system and remove auth routes when `false` #### `authentication.mode` [#authenticationmode] * **Type**: `"static" | "dynamic"` * **Required**: Yes * **Description**: Defines whether to use Static or Dynamic Role-Based Access Control #### `authentication.login.allowedUsernames` [#authenticationloginallowedusernames] * **Type**: `string[]` * **Default**: `["username"]` * **Description**: Fields that can be used as username for authentication #### `authentication.login.sendAccessTokenThrough` [#authenticationloginsendaccesstokenthrough] * **Type**: `"cookie-only" | "response-only" | "both"` * **Default**: `"both"` * **Description**: How to return access tokens after login #### `authentication.rateLimit` [#authenticationratelimit] * **Type**: `Partial` * **Default**: `{ windowMs: 5000, limit: 10 }` * **Description**: Rate limiting for authentication endpoints #### `authentication.jwt` [#authenticationjwt] * **Type**: Object containing JWT configuration * **Description**: JWT token settings ### Validation Configuration [#validation-configuration] ```typescript // arkos.config.ts const arkosConfig: ArkosConfig = { validation: { resolver: "zod", strict: false, validationOptions: { // Zod or class-validator options }, }, }; export default arkosConfig; ``` ```typescript // src/app.ts arkos.init({ validation: { resolver: "zod", validationOptions: { // Zod or class-validator options }, }, }); ``` #### `validation.resolver` [#validationresolver] * **Type**: `"class-validator" | "zod"` * **Required**: Yes * **Description**: Validation library to use #### `validation.strict` [#validationstrict] * **Type**: `boolean` * **Default**: `false` * **Description**: Require validation configuration for all ArkosRouter endpoints #### `validation.validationOptions` [#validationvalidationoptions] * **Type**: `ValidatorOptions` or `Record` * **Description**: Options passed to the validation library ### File Upload Configuration [#file-upload-configuration] ```typescript // arkos.config.ts const arkosConfig: ArkosConfig = { fileUpload: { baseUploadDir: "/uploads", baseRoute: "/api/uploads", expressStatic: { maxAge: "1y", etag: true, }, restrictions: { images: { maxCount: 10, maxSize: 5 * 1024 * 1024, // 5MB supportedFilesRegex: /\.(jpg|jpeg|png|gif|webp)$/, }, }, }, }; export default arkosConfig; ``` ```typescript // src/app.ts arkos.init({ fileUpload: { baseUploadDir: "/uploads", baseRoute: "/api/uploads", expressStaticOptions: { maxAge: "1y", etag: true, }, // ... restrictions }, }); ``` #### `fileUpload.baseUploadDir` [#fileuploadbaseuploaddir] * **Type**: `string` * **Default**: `"/uploads"` * **Description**: Base directory for file uploads #### `fileUpload.baseRoute` [#fileuploadbaseroute] * **Type**: `string` * **Default**: `"/api/uploads"` * **Description**: Base route for file access #### `fileUpload.expressStatic` [#fileuploadexpressstatic] * **Type**: `Parameters[1]` * **Description**: Options for express.static middleware #### `fileUpload.restrictions` [#fileuploadrestrictions] * **Type**: Object containing file type restrictions * **Description**: Upload restrictions for different file types ### Middleware Configuration [#middleware-configuration] ```typescript // arkos.config.ts const arkosConfig: ArkosConfig = { middlewares: { compression: { level: 6, }, rateLimit: { windowMs: 60000, limit: 1000, }, cors: { allowedOrigins: ["https://example.com"], options: { credentials: true, }, }, expressJson: { limit: "10mb", }, cookieParser: ["secret"], queryParser: { parseNull: true, parseBoolean: true, parseDoubleUnderscore: true, }, requestLogger: myCustomLogger, errorHandler: myCustomErrorHandler, }, }; export default arkosConfig; ``` **Disabling Middlewares:** ```typescript const arkosConfig: ArkosConfig = { middlewares: { compression: false, // Disable compression rateLimit: false, // Disable rate limiting requestLogger: false, // Disable request logger }, }; export default arkosConfig; ``` **Replacing Middlewares:** ```typescript const arkosConfig: ArkosConfig = { middlewares: { cors: myCustomCorsHandler, // Replace with custom handler errorHandler: myErrorHandler, // Replace with custom handler }, }; export default arkosConfig; ``` ```typescript // src/app.ts arkos.init({ globalRequestRateLimitOptions: { windowMs: 60000, limit: 1000, }, jsonBodyParserOptions: { limit: "10mb", }, cookieParserParameters: ["secret"], compressionOptions: { level: 6, }, queryParserOptions: { parseNull: true, parseBoolean: true, }, cors: { allowedOrigins: ["https://example.com"], options: { credentials: true, }, }, middlewares: { additional: [myCustomMiddleware], disable: ["compression", "request-logger"], replace: { cors: myCustomCorsHandler, globalErrorHandler: myCustomErrorHandler, }, }, }); ``` #### Middleware Options [#middleware-options] * **compression**: `false | CompressionOptions | ArkosRequestHandler` * **rateLimit**: `false | Partial | ArkosRequestHandler` * **cors**: `false | CorsConfig | ArkosRequestHandler` * **expressJson**: `false | express.JsonOptions | ArkosRequestHandler` * **cookieParser**: `false | Parameters | ArkosRequestHandler` * **queryParser**: `false | QueryParserOptions | ArkosRequestHandler` * **requestLogger**: `false | ArkosRequestHandler` * **errorHandler**: `false | express.ErrorRequestHandler` ### Router Configuration [#router-configuration] ```typescript // arkos.config.ts const arkosConfig: ArkosConfig = { routers: { strict: "no-bulk", welcomeRoute: (req, res) => { res.json({ message: "Custom welcome message" }); }, }, }; export default arkosConfig; ``` ```typescript // src/app.ts - Register custom routers import arkos from "arkos"; import customRouter from "./routers/custom.router"; import expressRouter from "./routers/express.router"; arkos.init({ use: [customRouter, expressRouter], // ArkosRouter or Express Router }); ``` ```typescript // src/app.ts arkos.init({ routers: { strict: "no-bulk", additional: [customRouter], disable: ["welcome-endpoint"], replace: { welcomeEndpoint: (req, res) => { res.json({ message: "Custom welcome" }); }, }, }, }); ``` #### `routers.strict` [#routersstrict] * **Type**: `boolean | "no-bulk"` * **Default**: `false` * **Description**: Strict mode for routing security (Disables all auto generated endpoints) #### `routers.welcomeRoute` [#routerswelcomeroute] * **Type**: `false | ArkosRequestHandler` * **Description**: Custom welcome endpoint handler or `false` to disable ### Advanced Configuration [#advanced-configuration] ```typescript // src/app.ts import arkos from "arkos"; import customRouter from "./routers/custom.router"; arkos.init({ use: [customRouter], configureApp: async (app) => { app.set("trust proxy", 1); // Custom app configuration }, configureServer: (server) => { server.timeout = 30000; // Custom server configuration }, }); ``` ```typescript // src/app.ts arkos.init({ routers: { additional: [customRouter], }, configureApp: async (app) => { app.set("trust proxy", 1); }, configureServer: (server) => { server.timeout = 30000; }, }); ``` #### `use` [#use] * **Type**: `(IArkosRouter | express.Router | ArkosRequestHandler | ArkosErrorRequestHandler)[]` * **Description**: Custom routers and middlewares to add to the application #### `configureApp` [#configureapp] * **Type**: `(app: express.Express) => any` * **Description**: Function to configure the Express app instance #### `configureServer` [#configureserver] * **Type**: `(server: http.Server) => any` * **Description**: Function to configure the HTTP server instance ### Email Configuration [#email-configuration] ```typescript // arkos.config.ts const arkosConfig: ArkosConfig = { email: { name: "My App", host: "smtp.example.com", port: 587, secure: false, auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASSWORD, }, }, }; export default arkosConfig; ``` ```typescript // src/app.ts arkos.init({ email: { name: "My App", host: "smtp.example.com", port: 587, secure: false, auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASSWORD, }, }, }); ``` #### `email.host` [#emailhost] * **Type**: `string` * **Required**: Yes * **Description**: SMTP host #### `email.port` [#emailport] * **Type**: `number` * **Default**: `465` * **Description**: SMTP port #### `email.secure` [#emailsecure] * **Type**: `boolean` * **Default**: `true` * **Description**: Use secure connection #### `email.auth.user` [#emailauthuser] * **Type**: `string` * **Required**: Yes * **Description**: SMTP username #### `email.auth.pass` [#emailauthpass] * **Type**: `string` * **Required**: Yes * **Description**: SMTP password #### `email.name` [#emailname] * **Type**: `string` * **Description**: Display name for sent emails ### Swagger Configuration [#swagger-configuration] ```typescript // arkos.config.ts const arkosConfig: ArkosConfig = { swagger: { enableAfterBuild: true, endpoint: "/api/docs", mode: "zod", strict: false, options: { definition: { info: { title: "My API", version: "1.0.0", description: "API documentation", }, servers: [{ url: "http://localhost:3000" }], }, deepLinking: true, tryItOutEnabled: true, }, scalarApiReferenceConfiguration: { theme: "bluePlanet", }, }, }; export default arkosConfig; ``` ```typescript // src/app.ts arkos.init({ swagger: { enableAfterBuild: true, endpoint: "/api/docs", mode: "zod", // ... other options }, }); ``` #### `swagger.enableAfterBuild` [#swaggerenableafterbuild] * **Type**: `boolean` * **Default**: `false` * **Description**: Enable API documentation after build #### `swagger.endpoint` [#swaggerendpoint] * **Type**: `string` * **Default**: `"/api/api-docs"` * **Description**: Swagger UI endpoint #### `swagger.mode` [#swaggermode] * **Type**: `"prisma" | "class-validator" | "zod"` * **Required**: Yes * **Description**: Schema generation mode #### `swagger.strict` [#swaggerstrict] * **Type**: `boolean` * **Default**: `false` * **Description**: Strict schema validation ### Request Configuration [#request-configuration] ```typescript // arkos.config.ts const arkosConfig: ArkosConfig = { request: { parameters: { allowDangerousPrismaQueryOptions: false, }, }, }; export default arkosConfig; ``` ```typescript // src/app.ts arkos.init({ request: { parameters: { allowDangerousPrismaQueryOptions: false, }, }, }); ``` #### `request.parameters.allowDangerousPrismaQueryOptions` [#requestparametersallowdangerousprismaqueryoptions] * **Type**: `boolean` * **Default**: `false` * **Description**: Allow passing Prisma query options in request parameters ### Debugging Configuration [#debugging-configuration] > Available from `v1.4.0-beta` ```typescript // arkos.config.ts const arkosConfig: ArkosConfig = { debugging: { requests: { level: 1, filter: ["Query", "Body"], }, dynamicLoader: { level: 2, filters: { modules: ["user", "product"], components: ["router", "service"], }, }, }, }; export default arkosConfig; ``` ## Environment Variables [#environment-variables] Arkos.js supports the following environment variables: | Variable | Description | Default | Required | | ---------------------- | ------------------------------------------------------------------ | ---------------------------------------------- | --------------------------------- | | `PORT` | Application port number | `8000` | No | | `NODE_ENV` | Application environment mode (`development`, `production`, `test`) | `development` | No | | `HOST` | Host to bind the server to | `localhost` | No | | `DATABASE_URL` | Database connection string | - | **Yes** | | `JWT_SECRET` | Secret key for JWT token signing and verification | - | **Yes** (if using authentication) | | `JWT_EXPIRES_IN` | JWT token expiration time (e.g., "30d", "2h", "3600") | `30d` | No | | `JWT_COOKIE_SECURE` | Whether JWT cookie is sent only over HTTPS | `true` in production, `false` in development | No | | `JWT_COOKIE_HTTP_ONLY` | Whether JWT cookie is HTTP-only (inaccessible to JavaScript) | `true` | No | | `JWT_COOKIE_SAME_SITE` | SameSite attribute for JWT cookie (`lax`, `strict`, `none`) | `"none"` in production, `"lax"` in development | No | | `EMAIL_HOST` | SMTP server host for email service | - | No | | `EMAIL_PORT` | SMTP server port | `465` | No | | `EMAIL_SECURE` | Use secure SMTP connection | `true` | No | | `EMAIL_USER` | SMTP authentication username/email | - | No | | `EMAIL_PASSWORD` | SMTP authentication password | - | No | | `EMAIL_NAME` | Display name for sent emails | - | No | ## Complete Example [#complete-example] ```typescript // arkos.config.ts import { ArkosConfig } from "arkos"; const arkosConfig: ArkosConfig = { port: 3000, host: "0.0.0.0", welcomeMessage: "Welcome to Our API", authentication: { enabled: true, mode: "static", jwt: { secret: process.env.JWT_SECRET, expiresIn: "7d", }, }, validation: { resolver: "zod", strict: false, }, fileUpload: { baseUploadDir: "/uploads", restrictions: { images: { maxCount: 5, maxSize: 5 * 1024 * 1024, }, }, }, middlewares: { cors: { allowedOrigins: ["https://myapp.com"], }, rateLimit: { windowMs: 60000, limit: 500, }, }, routers: { strict: "no-bulk", }, email: { host: process.env.EMAIL_HOST, auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASSWORD, }, }, swagger: { mode: "zod", enableAfterBuild: false, }, }; export default arkosConfig; ``` ```typescript // src/app.ts import arkos from "arkos"; import analyticsRouter from "./routers/analytics.router"; arkos.init({ use: [analyticsRouter], configureApp: (app) => { app.set("trust proxy", 1); }, }); ``` ```typescript // src/app.ts import arkos from "arkos"; import analyticsRouter from "./routers/analytics.router"; arkos.init({ port: 3000, host: "0.0.0.0", welcomeMessage: "Welcome to Our API", authentication: { mode: "static", jwt: { secret: process.env.JWT_SECRET, expiresIn: "7d", }, }, validation: { resolver: "zod", }, fileUpload: { baseUploadDir: "/uploads", restrictions: { images: { maxCount: 5, maxSize: 5 * 1024 * 1024, }, }, }, globalRequestRateLimitOptions: { windowMs: 60000, limit: 500, }, cors: { allowedOrigins: ["https://myapp.com"], }, routers: { strict: "no-bulk", additional: [analyticsRouter], }, email: { host: process.env.EMAIL_HOST, auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASSWORD, }, }, swagger: { mode: "zod", enableAfterBuild: false, }, configureApp: (app) => { app.set("trust proxy", 1); }, }); ``` ## Configuration Precedence [#configuration-precedence] Configuration values are loaded in this order (highest priority first): 1. Values passed directly to `arkos.init()` (v1.4) or in `arkos.config.ts` (v1.4) 2. Environment variables 3. Default values provided by Arkos.js ## Related Guides [#related-guides] * Learn about [Arkos Router API Reference](/docs/reference/arkos-router) * Explore the [Authentication System Guide](/docs/core-concepts/authentication/setup) * Read about [Request Data Validation](/docs/guides/validation/setup) # Arkos Prisma Input > Available since `v1.5.0-beta` A TypeScript utility type that simplifies Prisma relation operations by flattening nested `create`, `connect`, `update`, and `delete` operations into an intuitive array-based format with automatic operation detection. ## Overview [#overview] When working with Prisma relations, you typically need to write verbose nested objects: ```typescript // Prisma's default way const user: Prisma.UserCreateInput = { name: "John", posts: { create: [{ title: "Post 1" }], connect: [{ id: 1 }], update: [ { where: { id: 2 }, data: { title: "Updated" } } ] } }; ``` `ArkosPrismaInput` transforms this into a simpler, more intuitive format: ```typescript import { ArkosPrismaInput } from "arkos/prisma"; import { Prisma } from "@prisma/client"; // Arkos way - cleaner and more intuitive const user: ArkosPrismaInput = { name: "John", posts: [ { title: "Post 1" }, // auto-detects: create { id: 1 }, // auto-detects: connect { id: 2, title: "Updated" }, // auto-detects: update { id: 3, apiAction: "delete" } // explicit: delete ] }; ``` ## Import [#import] ```typescript import { ArkosPrismaInput } from "arkos/prisma"; ``` ## Type Signature [#type-signature] ```typescript type ArkosPrismaInput = FlattenRelations; ``` Where `T` is any Prisma input type (e.g., `Prisma.UserCreateInput`, `Prisma.PostUpdateInput`). ## How It Works [#how-it-works] ### Automatic Operation Detection [#automatic-operation-detection] The utility type analyzes the fields present in each relation object to determine the operation: | Fields Present | Detected Operation | Example | | ----------------------- | ------------------ | -------------------------------- | | Only create fields | `create` | `{ title: "New Post" }` | | Only unique identifiers | `connect` | `{ id: 1 }` | | Unique ID + data fields | `update` | `{ id: 1, title: "Updated" }` | | `apiAction: "delete"` | `delete` | `{ id: 1, apiAction: "delete" }` | ### Explicit Operation Control [#explicit-operation-control] For ambiguous cases, use the `apiAction` discriminator: ```typescript const user: ArkosPrismaInput = { posts: [ { id: 1 }, // Ambiguous - could be connect or update { id: 2, apiAction: "connect" }, // Explicit connect { id: 3, apiAction: "update", title: "Updated" } // Explicit update ] }; ``` ## Supported Operations [#supported-operations] The type utility supports all Prisma relation operations: * **`create`** - Create new related records * **`connect`** - Link to existing records by unique identifier * **`update`** - Update existing related records * **`delete`** - Delete related records * **`disconnect`** - Remove relationship without deleting * **`deleteMany`** - Delete multiple related records ## Usage Examples [#usage-examples] ### One-to-Many Relations [#one-to-many-relations] ```typescript import { ArkosPrismaInput } from "arkos/prisma"; import { Prisma } from "@prisma/client"; type CreateUserInput = ArkosPrismaInput; const newUser: CreateUserInput = { name: "Alice", email: "alice@example.com", posts: [ // Create new posts { title: "First Post", content: "Hello World" }, { title: "Second Post", content: "Learning Arkos" }, // Connect existing posts { id: 10 }, { id: 15 }, // Update existing post { id: 20, title: "Updated Title" } ] }; ``` ### One-to-One Relations [#one-to-one-relations] ```typescript type UpdateUserInput = ArkosPrismaInput; const userUpdate: UpdateUserInput = { name: "Alice Updated", profile: { // For singular relations, can use either format bio: "Software Developer", avatar: "avatar.jpg" } // OR use apiAction for explicit control profile: { id: 1, apiAction: "update", bio: "Updated bio" } }; ``` ### Nested Relations [#nested-relations] The type works recursively for deeply nested relations: ```typescript type CreatePostInput = ArkosPrismaInput; const newPost: CreatePostInput = { title: "My Post", author: { id: 1 // Connect to existing author }, comments: [ { content: "Great post!", author: { id: 2 // Nested relation - connect to commenter } }, { content: "Thanks for sharing", author: { name: "Anonymous", // Nested relation - create new user email: "anon@example.com" } } ] }; ``` ### Explicit Operations [#explicit-operations] ```typescript const userUpdate: ArkosPrismaInput = { posts: [ // Create { title: "New Post", apiAction: "create" }, // Connect { id: 1, apiAction: "connect" }, // Update { id: 2, title: "Updated", apiAction: "update" }, // Delete { id: 3, apiAction: "delete" }, // Disconnect (remove relation without deleting) { id: 4, apiAction: "disconnect" }, ] }; ``` ## Integration with Arkos [#integration-with-arkos] ### With Interceptor Middlewares [#with-interceptor-middlewares] Perfect for type-safe request body manipulation: ```typescript import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; import { Prisma } from "@prisma/client"; import { ArkosPrismaInput } from "arkos/prisma"; type CreateUserBody = ArkosPrismaInput; export const beforeCreateOne = [ async ( req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { // Type-safe access to flattened relations if (!req.body.profile) { req.body.profile = { bio: "New user", isPublic: true }; } // Ensure all posts are created with draft status if (req.body.posts) { req.body.posts = req.body.posts.map(post => ({ ...post, status: "draft" })); } next(); } ]; ``` ### With Validation Schemas [#with-validation-schemas] Combine with Zod or class-validator for complete type safety: ```typescript import z from "zod"; import { ArkosPrismaInput } from "arkos/prisma"; import { Prisma } from "@prisma/client"; // Define Zod schema const CreateUserSchema = z.object({ name: z.string(), email: z.string().email(), posts: z.array(z.object({ title: z.string(), content: z.string(), apiAction: z.enum(["create", "connect", "update"]).optional() })).optional() }); // Merge with Prisma types type CreateUserInput = z.infer & ArkosPrismaInput; // Use in interceptor export const addDefaults = async ( req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { // Full type safety from both schema and Prisma if (!req.body.posts) { req.body.posts = []; } next(); }; ``` ### With Custom Controllers [#with-custom-controllers] ```typescript import { ArkosRequest, ArkosResponse } from "arkos"; import { Prisma } from "@prisma/client"; import { ArkosPrismaInput } from "arkos/prisma"; import userService from "./user.service"; type CreateUserBody = ArkosPrismaInput; class UserController { async createWithPosts( req: ArkosRequest, res: ArkosResponse ) { const { name, email, posts } = req.body; // Type-safe body access const user = await userService.createOne({ name, email, posts // Arkos auto-handles the flattened format }); res.json(user); } } ``` ## Automatic Relation Handling [#automatic-relation-handling] This type utility pairs perfectly with Arkos's built-in relation handling that's been available since the beginning. When you use `ArkosPrismaInput` with Arkos services, the framework automatically converts the flattened format into proper Prisma operations. ```typescript // In your controller or interceptor const userData: ArkosPrismaInput = { name: "John", posts: [ { title: "Post 1" }, // Arkos converts to: { create: { title: "Post 1" } } { id: 1 } // Arkos converts to: { connect: { id: 1 } } ] }; // BaseService automatically handles the conversion await userService.createOne(userData); ``` For more details on how Arkos handles relations, see [Handling Prisma Relation Fields](/docs/core-concepts/prisma-orm/handling-relations). ## Type Safety Features [#type-safety-features] ### Mutual Exclusivity for Singular Relations [#mutual-exclusivity-for-singular-relations] For one-to-one relations, the type enforces mutual exclusivity between flattened and Prisma formats: ```typescript type UpdateUserInput = ArkosPrismaInput; const update: UpdateUserInput = { profile: { bio: "New bio" // ✅ Flattened format } }; // OR const update2: UpdateUserInput = { profile: { update: { // ✅ Prisma format where: { id: 1 }, data: { bio: "New bio" } } } }; // But NOT mixed const invalid: UpdateUserInput = { profile: { bio: "New bio", // ❌ Can't mix both formats update: { ... } } }; ``` ### Non-Relation Fields Preserved [#non-relation-fields-preserved] The type utility only affects relation fields - scalar fields remain unchanged: ```typescript type CreateUserInput = ArkosPrismaInput; const user: CreateUserInput = { // Scalar fields - unchanged name: "John", email: "john@example.com", age: 30, // Relation fields - flattened posts: [ { title: "Post 1" } ] }; ``` ## Advanced Usage [#advanced-usage] ### Dynamic Operation Types [#dynamic-operation-types] Use TypeScript discriminated unions for runtime type narrowing: ```typescript type PostOperation = | { action: "create"; title: string; content: string } | { action: "connect"; id: number } | { action: "update"; id: number; title?: string } | { action: "delete"; id: number }; const createUser = (operations: PostOperation[]) => { const userData: ArkosPrismaInput = { name: "User", posts: operations.map(op => { switch (op.action) { case "create": return { title: op.title, content: op.content }; case "connect": return { id: op.id }; case "update": return { id: op.id, title: op.title, apiAction: "update" }; case "delete": return { id: op.id, apiAction: "delete" }; } }) }; return userService.createOne(userData); }; ``` ### Type Guards [#type-guards] Create type guards for safer runtime checks: ```typescript function isCreateOperation(rel: any): rel is { title: string } { return 'title' in rel && !('id' in rel); } function isConnectOperation(rel: any): rel is { id: number } { return 'id' in rel && !('title' in rel) && !('apiAction' in rel); } function isUpdateOperation(rel: any): rel is { id: number; title?: string; apiAction: "update" } { return 'id' in rel && ('title' in rel || rel.apiAction === 'update'); } // Use in your code const processRelation = (rel: any) => { if (isCreateOperation(rel)) { console.log("Creating:", rel.title); } else if (isConnectOperation(rel)) { console.log("Connecting:", rel.id); } else if (isUpdateOperation(rel)) { console.log("Updating:", rel.id); } }; ``` ## Limitations [#limitations] 1. **No Runtime Transformation**: This is a type-only utility. The actual runtime transformation is handled by Arkos's relation handling system when using [BaseService](/docs/reference/base-service) methods. 2. **Requires Arkos Services**: For the flattened format to work, you must use Arkos's BaseService or custom services that extend it. Direct Prisma Client calls require traditional Prisma format. 3. **Ambiguous Cases**: When an object could be either `connect` or `update`, explicitly use `apiAction` to disambiguate. ## Best Practices [#best-practices] 1. **Use with Interceptors**: Combine with interceptor middlewares for type-safe request manipulation 2. **Explicit Actions**: When in doubt, use `apiAction` for clarity 3. **Validation Integration**: Pair with Zod/class-validator schemas for complete type safety 4. **Leverage Auto-Detection**: Let the type utility auto-detect operations when possible 5. **Document Complex Relations**: Add comments for complex nested relation operations ## Related Documentation [#related-documentation] * **[Handling Prisma Relation Fields](/docs/core-concepts/prisma-orm/handling-relations)** - How Arkos handles relations at runtime * **[Interceptor Middlewares](/docs/core-concepts/components/interceptors)** - Using with request interceptors * **[BaseService Class](/docs/reference/base-service)** - Service layer integration * **[Request Data Validation](/docs/guides/validation/setup)** - Combining with validation schemas # Arkos Router > Available from `v1.4.0-beta` A comprehensive reference for configuring routes with ArkosRouter, Arkos's enhanced Express Router that provides declarative configuration for authentication, validation, rate limiting, file uploads, and more. ## Overview [#overview] ArkosRouter extends Express Router with a configuration-first approach. Instead of chaining middleware functions, you define your route's behavior through a configuration object: ```typescript import { ArkosRouter } from "arkos"; const router = ArkosRouter({ prefix: "/api/users" }); router.get( { path: "/:id", authentication: true, validation: { params: z.object({ id: z.string() }), }, }, userController.getUser ); ``` This declarative approach keeps your routes clean, self-documenting, and consistent across your application. :::tip The `prefix` option in `ArkosRouter({ prefix: "/api/users" })` was able from [v1.5.0](/blog/1.5-beta) for DX improvements. ::: ## Configuration Object [#configuration-object] The first argument to any HTTP method (`get`, `post`, `put`, `patch`, `delete`) is a configuration object with the following properties: ### `path` (required) [#path-required] The route path following Express routing conventions. ```typescript router.get({ path: "/api/users" }, handler); router.get({ path: "/api/users/:id" }, handler); router.get({ path: "/api/posts/:postId/comments/:commentId" }, handler); ``` **Path Parameters**: Use Express parameter syntax (`:paramName`). These can be validated using the `validation.params` option. ### `disabled` [#disabled] Completely disables the route. Useful for temporarily removing endpoints without deleting code. ```typescript router.post( { path: "/api/admin/dangerous-action", disabled: true, // This endpoint won't be registered }, handler ); ``` ## Authentication [#authentication] Control authentication and role-based access control for your routes. ### Basic Authentication [#basic-authentication] Require users to be authenticated: ```typescript router.get( { path: "/api/profile", authentication: true, }, handler ); ``` ### Role-Based Access Control [#role-based-access-control] Define which roles can access the endpoint: ```typescript router.post( { path: "/api/posts", authentication: { resource: "post", action: "Create", rule: { roles: ["Admin", "Editor"] }, // When using static authentication }, }, handler ); ``` **Authentication Object Properties:** | Property | Type | Description | | ---------- | ----------------------------------- | --------------------------------------------------------------------------------------------------- | | `resource` | `string` | The resource being accessed (e.g., "post", "user") | | `action` | `string` | The action being performed (e.g., "Create", "Update", "Delete") | | `rule` | `{ roles: string[] }` or `string[]` | Array of role names that can perform this action (This only works when using static authentication) | :::tip The `rule` field is required for defining roles when you are using the [Static Authentication Mode](/docs/core-concepts/authentication/setup#jwt-configuration--environment-setup). This field can be an object `{ roles: string[] }` and you can add many other descriptive fields to easy your frontend devs lives or it can be a simple array of string `string[]` which will be converted to an object under the hood. You can more about detailed access control rules at [Adding Auth Configs File Guide](/docs/core-concepts/authentication/setup#auth-config-files---static-rbac). ::: **Example with Custom Actions:** ```typescript router.post( { path: "/api/posts/:id/publish", authentication: { resource: "post", action: "Publish", rule: { roles: ["Admin", "Editor"] }, // When using static authentication }, }, handler ); ``` For complete authentication setup and advanced features, see the [Authentication System Guide](/docs/core-concepts/authentication/setup). ## Validation [#validation] Validate incoming request data using Zod schemas or class-validator DTOs. ### Request Body Validation [#request-body-validation] ```typescript import z from "zod"; const CreateUserSchema = z.object({ name: z.string().min(2), email: z.string().email(), age: z.number().min(18).optional(), }); router.post( { path: "/api/users", validation: { body: CreateUserSchema, }, }, handler ); ``` ### Query Parameters Validation [#query-parameters-validation] ```typescript const SearchQuerySchema = z.object({ q: z.string(), limit: z.number().int().min(1).max(100).default(10), offset: z.number().int().min(0).default(0), }); router.get( { path: "/api/search", validation: { query: SearchQuerySchema, }, }, handler ); ``` ### Path Parameters Validation [#path-parameters-validation] ```typescript const UserParamsSchema = z.object({ id: z.string().uuid(), }); router.get( { path: "/api/users/:id", validation: { params: UserParamsSchema, }, }, handler ); ``` ### Multiple Validation Targets [#multiple-validation-targets] ```typescript router.patch( { path: "/api/posts/:id", validation: { params: z.object({ id: z.string() }), body: UpdatePostSchema, query: z.object({ publish: z.boolean().optional() }), }, }, handler ); ``` **Validation Options:** | Property | Type | Description | | -------- | -------------------------------- | -------------------------- | | `body` | `ZodSchema \| ClassValidatorDto` | Validates request body | | `query` | `ZodSchema \| ClassValidatorDto` | Validates query parameters | | `params` | `ZodSchema \| ClassValidatorDto` | Validates URL parameters | For complete validation setup and class-validator usage, see the [Request Data Validation Guide](/docs/guides/validation/setup). ## Rate Limiting [#rate-limiting] Protect your endpoints from abuse by limiting request frequency. ### Basic Rate Limiting [#basic-rate-limiting] ```typescript router.post( { path: "/api/auth/login", rateLimit: { windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 requests per window }, }, handler ); ``` ### Custom Rate Limit Messages [#custom-rate-limit-messages] ```typescript router.post( { path: "/api/reports/generate", rateLimit: { windowMs: 60 * 60 * 1000, // 1 hour max: 3, message: "Report generation limit exceeded. Please try again in an hour.", }, }, handler ); ``` **Rate Limit Properties:** | Property | Type | Description | Default | | ---------- | -------- | --------------------------- | ------------------- | | `windowMs` | `number` | Time window in milliseconds | - | | `max` | `number` | Maximum requests per window | - | | `message` | `string` | Custom error message | "Too many requests" | **Note**: Route-level rate limits override global configuration. If you've set rate limiting globally in `arkos.config.ts`, specifying it here will use these values instead. ## File Uploads [#file-uploads] Handle file uploads with automatic validation and processing. ### Single File Upload [#single-file-upload] ```typescript router.post( { path: "/api/users/avatar", authentication: true, experimental: { uploads: { type: "single", field: "avatar", maxSize: 1024 * 1024 * 5, // 5MB uploadDir: "avatars", required: true, // (Defaults) added in v1.5.0 }, }, }, handler ); ``` ### Multiple Files Upload [#multiple-files-upload] ```typescript router.post( { path: "/api/gallery", authentication: true, experimental: { uploads: { type: "array", field: "photos", maxCount: 10, uploadDir: "gallery", allowedFileTypes: [".jpg", ".png", ".webp"], required: true, // (Defaults) added in v1.5.0 }, }, }, handler ); ``` ### Multiple Fields Upload [#multiple-fields-upload] ```typescript router.post( { path: "/api/products", authentication: true, experimental: { uploads: { type: "fields", fields: [ { name: "thumbnail", maxCount: 1 }, { name: "images", maxCount: 5 }, { name: "documents", maxCount: 3 }, ], uploadDir: "products", required: false, // Optinal files, added in v1.5.0 }, }, }, handler ); ``` ### Nested Fields Upload [#nested-fields-upload] Arkos Router also supports nested field names using bracket notation, making it easy to handle complex form structures. When using nested fields with `attachToBody` enabled (default), uploaded files are automatically organized in the correct nested structure within `req.body`. ```typescript router.post( { path: "/api/users", authentication: true, experimental: { uploads: { type: "single", field: "profile[photo]", // Nested field notation uploadDir: "users-profile", required: false, // Optinal user photo, added in v1.5.0 }, }, }, handler ); ``` :::tip New Required Flag Notice that the `required` flag defaults to true, if ommited the file will be required yet, so if you want to make it optional pass `required: false`. Is important to know that this feature of `required` flag was added at [v1.5.0+](/blog/1.5-beta). ::: **Result in `req.body`:** ```typescript // console.log(req.body) { name: "Luis Juliano", email: "luis@becas.co.mz", profile: { photo: "/images/my-profile-photo-34123843219438.jpg" // Nested correctly }, birthday: "1999-08-03" } ``` **Multiple Nested Files:** ```typescript router.post( { path: "/api/products", authentication: true, experimental: { uploads: { type: "fields", fields: [ { name: "product[thumbnail]", maxCount: 1 }, { name: "product[gallery]", maxCount: 5 }, { name: "documents[manual]", maxCount: 1 }, { name: "documents[warranty]", maxCount: 1 }, ], uploadDir: "products", }, }, }, handler ); ``` **Result in `req.body`:** ```typescript { name: "Laptop Pro", price: 1299.99, product: { thumbnail: "/images/laptop-thumb.jpg", gallery: [ "/images/laptop-1.jpg", "/images/laptop-2.jpg", "/images/laptop-3.jpg" ] }, documents: { manual: "/documents/laptop-manual.pdf", warranty: "/documents/warranty-card.pdf" } } ``` **Deep Nesting:** ```typescript router.post( { path: "/api/company/profile", experimental: { uploads: { type: "single", field: "company[details][logo]", // Deep nesting uploadDir: "company-logos", }, }, }, handler ); ``` **Result in `req.body`:** ```typescript { company: { name: "Tech Corp", details: { logo: "/images/techcorp-logo.png" // Deeply nested } } } ``` **Important Notes:** * Bracket notation (`field[nested]`) is automatically parsed into nested objects even for non file upload fields * Works with all upload types: `single`, `array`, and `fields` * Only applies when `attachToBody` is not `false` (it's `"pathname"` by default) * The nested structure is created even if other fields in the path don't exist in the request **Upload Configuration Properties:** | Property | Type | Description | Default | | ------------------ | ---------------------------------------- | -------------------------------------- | -------------------------- | | `type` | `"single" \| "array" \| "fields"` | Upload type | - | | `field` | `string` | Form field name (single/array) | - | | `fields` | `Array<{name, maxCount}>` | Multiple field config (fields type) | - | | `uploadDir` | `string` | Storage directory | Auto-detected by MIME type | | `required` | `boolean` (from 1.5.0+) | Require or not | true | | `maxSize` | `number` | Max file size in bytes | From global config | | `maxCount` | `number` | Max files (array type) | - | | `allowedFileTypes` | `string[] \| RegExp` | Allowed file extensions/patterns | From global config | | `attachToBody` | `"pathname" \| "url" \| "file" \| false` | How to attach file info to req.body | `"pathname"` | | `deleteOnError` | `boolean` | Delete uploaded files if request fails | `false` | **Accessing Uploaded Files:** ```typescript // Single file const file = req.file; // Array of files const files = req.files; // Fields (multiple) const files = req.files as { [fieldname: string]: Express.Multer.File[] }; ``` For complete upload configuration and advanced features, see the [File Upload Guide](/docs/guides/file-handling/file-uploads/setup). ## OpenAPI Documentation [#openapi-documentation] Generate interactive API documentation automatically from your route configuration. ### Basic Documentation [#basic-documentation] ```typescript router.get( { path: "/api/users", experimental: { openapi: { summary: "List all users", description: "Retrieves a paginated list of users", tags: ["Users"], }, }, }, handler ); ``` ### Documentation with Responses [#documentation-with-responses] The power of ArkosRouter's OpenAPI integration is that you can use **Zod schemas, DTOs, or plain JSON Schema** directly—no need to write traditional OpenAPI response objects: ```typescript import z from "zod"; const UserSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email(), }); const ErrorSchema = z.object({ message: z.string(), code: z.number(), }); router.get( { path: "/api/users/:id", validation: { params: z.object({ id: z.string() }), }, experimental: { openapi: { summary: "Get user by ID", tags: ["Users"], responses: { 200: UserSchema, // Just pass the schema! 404: ErrorSchema, }, }, }, }, handler ); ``` ### Responses with Descriptions [#responses-with-descriptions] If you need custom descriptions, wrap your schema in an object: ```typescript router.post( { path: "/api/users", validation: { body: CreateUserSchema, }, experimental: { openapi: { summary: "Create a new user", tags: ["Users"], responses: { 201: { content: UserSchema, description: "User created successfully", }, 400: { content: ErrorSchema, description: "Invalid input data", }, 409: { content: ErrorSchema, description: "Email already exists", }, }, }, }, }, handler ); ``` ### Full OpenAPI Configuration [#full-openapi-configuration] For complete control, use the full OpenAPI response format: ```typescript router.post( { path: "/api/upload", experimental: { uploads: { type: "single", field: "file", }, openapi: { summary: "Upload file", requestBody: { content: { "multipart/form-data": { schema: FileUploadSchema, }, }, required: true, }, responses: { 200: { description: "File uploaded successfully", content: { "application/json": { schema: UploadResultSchema, }, }, }, }, }, }, }, handler ); ``` ### Excluding Routes from Documentation [#excluding-routes-from-documentation] ```typescript router.get( { path: "/api/internal/metrics", experimental: { openapi: false, // Won't appear in docs }, }, handler ); ``` **OpenAPI Configuration Properties:** | Property | Type | Description | | ------------- | ---------- | ---------------------------------- | | `summary` | `string` | Short description of the endpoint | | `description` | `string` | Detailed description | | `tags` | `string[]` | Groups endpoints in documentation | | `responses` | `object` | Response schemas by status code | | `requestBody` | `object` | Request body documentation | | `parameters` | `array` | Additional parameter documentation | **Important**: If you define validation in the `validation` field, **DO NOT** redefine the same schemas in `openapi`. Arkos automatically generates OpenAPI documentation from your validation schemas. The `experimental.openapi` field is for: * Adding metadata (summary, description, tags) * Documenting responses * Endpoints without validation For complete OpenAPI setup and configuration, see the [Swagger API Documentation Guide](/docs/guides/open-api-documentation/setup). ## Query Parsing [#query-parsing] ArkosRouter provides Django-style query parameter parsing that automatically transforms query strings into Prisma-compatible filters. ### How It Works [#how-it-works] Instead of manually constructing nested query objects, use double underscores (`__`) to define relationships and operators: ```typescript // Traditional approach GET /api/products?price[gte]=50&price[lt]=200&name[contains]=wireless // Django-style approach (cleaner!) GET /api/products?price__gte=50&price__lt=200&name__icontains=wireless ``` Both produce the same Prisma query, but the Django-style is more intuitive and easier to read. ### Examples [#examples] **Basic Filtering:** ```typescript // GET /api/users?age__gte=18&age__lt=65 // Transforms to: { age: { gte: 18, lt: 65 } } router.get( { path: "/api/users", queryParser: { parseDoubleUnderscore: true, // Enable Django-style parsing parseNumber: true, // "18" becomes 18 parseBoolean: true, // "true" becomes true }, }, handler ); ``` **String Search:** ```typescript // GET /api/products?name__icontains=phone&category__equals=Electronics // Transforms to: { name: { contains: "phone", mode: "insensitive" }, category: { equals: "Electronics" } } ``` **Nested Relations:** ```typescript // GET /api/posts?author__name__icontains=john // Transforms to: { author: { name: { contains: "john", mode: "insensitive" } } } ``` **Combining with Other Query Features:** ```typescript // GET /api/products?name__icontains=laptop&price__gte=500&sort=-price&limit=20 // Filters + sorting + pagination all work together ``` ### Query Parser Configuration [#query-parser-configuration] Configure how query parameters are parsed and transformed: ```typescript router.get( { path: "/api/products", queryParser: { parseDoubleUnderscore: true, // Enable Django-style operators parseNumber: true, // Convert numeric strings to numbers parseBoolean: true, // Convert "true"/"false" to booleans parseNull: true, // Convert "null" to null parseArray: true, // Parse comma-separated values as arrays }, }, handler ); ``` **Query Parser Properties:** | Property | Type | Description | Default | | ----------------------- | --------- | --------------------------------------------- | ------- | | `parseDoubleUnderscore` | `boolean` | Enable Django-style parsing (Arkos extension) | `true` | | `parseNumber` | `boolean` | Convert numeric strings to numbers | `true` | | `parseBoolean` | `boolean` | Convert "true"/"false" strings to booleans | `true` | | `parseNull` | `boolean` | Convert "null" string to null | `true` | | `parseArray` | `boolean` | Parse comma-separated values as arrays | `true` | For more examples and advanced filtering options, see the [Request Query Parameters Guide](/docs/core-concepts/prisma-orm/routes#querying). ## Body Parser [#body-parser] Customize how the request body is parsed for specific routes: ```typescript router.post( { path: "/api/webhooks/stripe", bodyParser: [ { parser: "raw", options: { type: "application/json" }, // Parse as raw buffer but only for application/json }, ], }, stripeWebhookHandler ); router.post( { path: "/api/data/upload", bodyParser: [ { parser: "text", options: { type: "text/plain", limit: "10mb" }, }, ], }, textDataHandler ); router.post( { path: "/api/users", bodyParser: [ { parser: "multipart", // Will pass only fields on multipart/form-data }, ], }, textDataHandler ); router.post( { path: "/api/form", bodyParser: [ { parser: "urlencoded", options: { extended: true }, }, ], }, formHandler ); router.post( { path: "/api/disable-parsing", bodyParser: false, // Disable body parsing entirely }, customParsingHandler ); ``` **Body Parser Configuration:** ```typescript bodyParser: { parser: "json" | "urlencoded" | "raw" | "text" | "multipart", options?: { /* parser-specific options */ } }[] // OR bodyParser: false // Disable parsing ``` **Parser Types:** | Parser | Description | Common Options | | -------------- | ----------------------------------------------------------------- | ------------------------------------- | | `"json"` | Parse as JSON (default globally) | `limit`, `strict`, `type` | | `"urlencoded"` | Parse as URL-encoded form data | `limit`, `extended`, `parameterLimit` | | `"raw"` | Parse as raw Buffer (for webhooks needing signature verification) | `limit`, `type` | | `"text"` | Parse as plain text | `limit`, `type`, `defaultCharset` | | `false` | Disable body parsing for this route | - | **Note**: By default, JSON parsing is enabled globally. Use this option to override the parser for specific routes, such as webhook endpoints that need raw request bodies for signature verification. ## Compression [#compression] Control response compression for specific routes, this is the same as the npm package compression you can check it at [Compression Github Repo](https://github.com/expressjs/compression). ```typescript router.get( { path: "/api/reports/large-dataset", compression: true, // Use default compression settings }, handler ); router.get( { path: "/api/reports/optimized", compression: { level: 6, // Compression level (0-9) threshold: "1kb", // Only compress responses larger than 1kb }, }, handler ); router.get( { path: "/api/stream/video", compression: false, // Disable compression for this route }, videoStreamHandler ); ``` ### Compression Configuration: [#compression-configuration] ```typescript compression: true | false | { level?: number; // 0-9, default: -1 (default compression) threshold?: number | string; // Minimum size to compress, default: "1kb" filter?: (req, res) => boolean; // Custom filter function memLevel?: number; // Memory level (1-9), default: 8 strategy?: number; // Compression strategy chunkSize?: number; // Chunk size for compression } ``` **Compression Options:** | Option | Type | Description | Default | | ----------- | ------------------ | ---------------------------------------------------------- | -------------------------- | | `level` | `number` | Compression level (0=none, 9=max) | `-1` (default) | | `threshold` | `number \| string` | Minimum response size to compress | `"1kb"` | | `filter` | `function` | Custom function to decide if response should be compressed | Uses `compressible` module | | `memLevel` | `number` | Memory allocated for compression (1-9) | `8` | | `strategy` | `number` | Compression strategy (zlib constants) | `Z_DEFAULT_STRATEGY` | | `chunkSize` | `number` | Chunk size in bytes | `16384` | **Common Use Cases:** ```typescript // High compression for large static reports compression: { level: 9, // Maximum compression threshold: "10kb" } // Fast compression for real-time data compression: { level: 1, // Fastest compression threshold: "5kb" } // Custom filter to exclude certain content types compression: { filter: (req, res) => { if (req.headers['x-no-compression']) { return false; } return compression.filter(req, res); } } ``` **Note**: Compression is enabled globally by default in Arkos. Use `compression: false` to disable it for specific routes where it's not beneficial (like streaming endpoints or pre-compressed content), or customize compression settings per route. ## Using Standard Express Middleware [#using-standard-express-middleware] While ArkosRouter provides declarative configuration for common needs, you can still use standard Express middleware alongside your handlers: ```typescript import { someMiddleware } from "./post.middlewares"; router.post( { path: "/api/posts", authentication: true, validation: { body: CreatePostSchema }, }, someMiddleware, anotherMiddleware, postController.create ); ``` Middleware functions execute in order before reaching your controller. This is standard Express behavior—ArkosRouter's configuration options are just convenient shortcuts for common middleware patterns. ## Complete Example [#complete-example] Here's a comprehensive example showing multiple features together: ```typescript import { ArkosRouter } from "arkos"; import z from "zod"; import postController from "./post.controller"; const router = ArkosRouter(); const CreatePostSchema = z.object({ title: z.string().min(5).max(200), content: z.string(), tags: z.array(z.string()).optional(), }); const PostResponseSchema = z.object({ id: z.string(), title: z.string(), content: z.string(), author: z.object({ id: z.string(), name: z.string(), }), createdAt: z.string(), }); router.post( { path: "/api/posts", authentication: { resource: "post", action: "Create", rule: ["Admin", "Editor"], }, validation: { body: CreatePostSchema, }, rateLimit: { windowMs: 60 * 1000, max: 10, }, queryParser: { parseDoubleUnderscore: true, }, experimental: { uploads: { type: "single", field: "featuredImage", uploadDir: "post-images", maxSize: 1024 * 1024 * 5, }, openapi: { summary: "Create a new blog post", description: "Creates a new post with optional featured image", tags: ["Posts"], responses: { 201: { content: PostResponseSchema, description: "Post created successfully", }, 400: { content: z.object({ message: z.string() }), description: "Invalid input", }, }, }, }, }, postController.create ); export default router; ``` ## Related Documentation [#related-documentation] * **[Adding Custom Routers](/docs/core-concepts/routing/setup)** - How to create and register custom routers * **[Authentication System](/docs/core-concepts/authentication/setup)** - Complete authentication setup * **[Request Data Validation](/docs/guides/validation/setup)** - Validation with Zod and class-validator * **[File Upload Guide](/docs/guides/file-handling/file-uploads/setup)** - File upload configuration * **[Swagger API Documentation](/docs/guides/open-api-documentation/setup)** - OpenAPI documentation setup * **[Request Query Parameters](/docs/core-concepts/prisma-orm/routes#querying)** - Advanced filtering and querying # Auth Service Object The `authService` object provides comprehensive authentication functionality for your Arkos application. While Arkos handles authentication automatically behind the scenes, you may need direct access to the authentication methods in your business logic. ## Accessing the Auth Service [#accessing-the-auth-service] The `authService` is automatically available in your Arkos application and can be imported directly: ```ts import { authService } from "arkos/services"; ``` :::warning On the following section you will various examples on how you can use the authService, you ain't restricted to those but also in the same time bear in mind that **Arkos** handles many of auth relate scenarios, such as authentication, authorization, password hashing, password checks and many others. ::: Before proceed reading this guide is highly encouragend to read the guide about how **Arkos** uses this under the hood so that you do not miss anything and try to reivent the wheel yourself, [Arkos Authentication Flow Guide](/docs/guides/validation/setup#authentication-endpoint-validation). ## API Reference [#api-reference] ### JWT Token Management [#jwt-token-management] #### `signJwtToken(id, expiresIn?, secret?)` [#signjwttokenid-expiresin-secret] Signs a JWT token for a user. **Parameters:** * `id` (number | string): The unique identifier of the user * `expiresIn` (optional): The expiration time for the token (defaults to JWT\_EXPIRES\_IN from environment) * `secret` (optional): The secret key for signing (defaults to JWT\_SECRET from environment) **Returns:** * A signed JWT token string **Example:** ```ts // In a custom middleware export const beforeCreateOne = catchAsync(async (req, res, next) => { // Generate a token with custom expiration for special access const temporaryToken = authService.signJwtToken(req.user.id, "4h"); req.body.temporaryAccessToken = temporaryToken; next(); }); ``` #### `verifyJwtToken(token, secret?)` [#verifyjwttokentoken-secret] Verifies the authenticity of a JWT token. **Parameters:** * `token` (string): The JWT token to verify * `secret` (optional): The secret key to use for verification **Returns:** * Promise resolving to the decoded JWT payload (AuthJwtPayload) **Throws:** * Error if the token is invalid or expired **Example:** ```ts // A custom middleware for API key validation export const validateApiKey = catchAsync(async (req, res, next) => { try { const apiKey = req.headers["x-api-key"] as string; const decoded = await authService.verifyJwtToken(apiKey); // Add custom authorization flags req.isApiRequest = true; req.apiClientId = decoded.id; next(); } catch (err) { next(new AppError("Invalid API key", 401)); } }); ``` ### Password Management [#password-management] #### `isCorrectPassword(candidatePassword, userPassword)` [#iscorrectpasswordcandidatepassword-userpassword] Compares a candidate password with the stored user password. **Parameters:** * `candidatePassword` (string): The password provided during login attempt * `userPassword` (string): The hashed password stored in the database **Returns:** * Promise resolving to boolean (true if passwords match) **Example:** ```ts // Custom password validation for sensitive operations export const beforeCreateOne = catchAsync(async (req, res, next) => { // Require password confirmation for critical operations if (!req.body.confirmPassword) { return next(new AppError("Please confirm your password", 400)); } const isValid = await authService.isCorrectPassword( req.body.confirmPassword, req.user.password ); if (!isValid) { return next(new AppError("Password confirmation failed", 401)); } // Remove password from request body to prevent accidental exposure delete req.body.confirmPassword; next(); }); ``` #### `hashPassword(password)` [#hashpasswordpassword] Hashes a plain text password using bcrypt. **Parameters:** * `password` (string): The plain text password to hash **Returns:** * Promise resolving to the hashed password string **Example:** ```ts // In a custom user invitation flow export const beforeCreateOne = catchAsync(async (req, res, next) => { // Generate a secure one-time password for invited users if (req.body.isInvitedUser) { const tempPassword = generateSecureRandomPassword(); req.body.password = await authService.hashPassword(tempPassword); req.body.passwordResetRequired = true; // Store the plain password temporarily for email sending req.tempPassword = tempPassword; } next(); }); ``` #### `isPasswordStrong(password)` [#ispasswordstrongpassword] Checks if a password meets strength requirements. **Parameters:** * `password` (string): The password to check **Returns:** * boolean (true if the password meets strength criteria) **Example:** ```ts // Add custom password policies beyond default requirements export const beforeUpdatePassword = catchAsync(async (req, res, next) => { const { newPassword } = req.body; // Or can use built-in validation through DTO or Schema if (!authService.isPasswordStrong(newPassword)) { return next( new AppError("Password doesn't meet security requirements", 400) ); } // Add additional custom password policy checks if (newPassword.includes(req.user.username)) { return next(new AppError("Password cannot contain your username", 400)); } next(); }); ``` ### User Authentication [#user-authentication] #### `userChangedPasswordAfter(user, JWTTimestamp)` [#userchangedpasswordafteruser-jwttimestamp] Checks if a user changed their password after a JWT was issued. **Parameters:** * `user` (User): The user object containing the passwordChangedAt field * `JWTTimestamp` (number): The timestamp when the JWT was issued **Returns:** * boolean (true if password was changed after JWT issuance) **Example:** ```ts // A custom middleware for API integrations export const validateLongTermToken = catchAsync(async (req, res, next) => { const integrationToken = req.headers["x-integration-token"]; if (!integrationToken) return next(); const decoded = await authService.verifyJwtToken(integrationToken); const user = await prisma.user.findUnique({ where: { id: decoded.id } }); if (!user) { return next(new AppError("Integration user not found", 401)); } // Check if password was changed, invalidating all tokens if (authService.userChangedPasswordAfter(user, decoded.iat)) { return next( new AppError( "Integration token expired due to password change", 401 ) ); } req.integrationUser = user; next(); }); ``` #### `getAuthenticatedUser(req)` [#getauthenticateduserreq] Retrieves the authenticated user from a request. **Parameters:** * `req` (ArkosRequest): The request object **Returns:** * Promise resolving to the authenticated User object or null **Throws:** * AppError if token is invalid or user not found **Example:** ```ts // Custom conditional authentication based on route context export const optionalAuthentication = catchAsync(async (req, res, next) => { try { // Try to authenticate but don't require it const user = await authService.getAuthenticatedUser(req); if (user) { req.user = user; req.isAuthenticated = true; } else { req.isAuthenticated = false; } // Content filtering logic based on authentication status if (!req.isAuthenticated) { req.query.isPublic = true; } next(); } catch (err) { // Continue as unauthenticated rather than failing req.isAuthenticated = false; req.query.isPublic = true; next(); } }); ``` #### `authenticate` [#authenticate] Middleware function to authenticate the user based on the JWT token. **Example:** ```ts // A custom router with specialized access control import { Router } from "express"; import { authService } from "arkos/services"; import { catchAsync } from "arkos/error-handler"; const router = Router(); // Only allow access during business hours const businessHoursOnly = catchAsync(async (req, res, next) => { const now = new Date(); const hour = now.getHours(); if (hour < 9 || hour >= 17) { return next( new AppError( "This API is only available during business hours", 403 ) ); } next(); }); // Route with custom authentication chain router.get( "/api/business-data", authService.authenticate, businessHoursOnly, (req, res) => { res.status(200).json({ status: "success", data: { message: "Welcome to the business API", user: req.user, }, }); } ); export default router; ``` ### Access Control Handlers [#access-control-handlers] #### `handleAccessControl(action, modelName, accessControlConfig)` [#handleaccesscontrolaction-modelname-accesscontrolconfig] Middleware function to handle access control based on user roles and permissions. **Parameters:** * `action` (AccessAction): The action being performed (e.g., Create, Update, Delete, View or custom actions) * `modelName` (string): The model name that the action is being performed on * `accessControlConfig` (AccessControlConfig): The configuration object for authentication and access control **Returns:** * Middleware function that checks if the user has permission **Example:** ```ts // Implementing department-specific access control import { authService } from "arkos/services"; import { catchAsync } from "arkos/error-handler"; const generateDepartmentReport = catchAsync(async (req, res, next) => { // Report generation logic const report = await generateReport(req.params.departmentId); res.status(200).json({ status: "success", data: report }); }); // Apply contextual access control with department check const protectedReportAccess = [ authService.authenticate, authService.handleAccessControl( "View", // action "report", // resource name ["Admin", "DepartmentHead"] // restricts only to those roles in static rbac ), // Additional custom department ownership check catchAsync(async (req, res, next) => { if ( req.user.role !== "Admin" && req.user.departmentId !== parseInt(req.params.departmentId) ) { return next( new AppError( "You can only access your own department reports", 403 ) ); } next(); }), generateDepartmentReport, ]; export { protectedReportAccess }; ``` #### `handleAuthenticationControl(action, authenticationControlConfig)` [#handleauthenticationcontrolaction-authenticationcontrolconfig] Handles authentication control by checking the configuration. **Parameters:** * `action` (AccessAction): The action being performed * `authenticationControlConfig` (AuthenticationControlConfig | undefined): The authentication configuration object **Returns:** * Middleware function that checks if authentication is required **Example:** ```ts // Implementing tiered access for different content levels import { authService } from "arkos/services"; import { catchAsync } from "arkos/error-handler"; const getContentData = catchAsync(async (req, res, next) => { let contentLevel = "basic"; // If authenticated, provide premium content if (req.isAuthenticated) { contentLevel = "premium"; } const data = await getContentByLevel(contentLevel); res.status(200).json({ status: "success", data }); }); // Apply optional authentication for tiered content const tieredContentAccess = [ authService.handleAuthenticationControl( "View", { View: false } // Not required but supported ), // Custom middleware to track authentication status catchAsync(async (req, res, next) => { req.isAuthenticated = !!req.user; next(); }), getContentData, ]; export { tieredContentAccess }; ``` ## Custom Actions and Extended Authentication [#custom-actions-and-extended-authentication] The `AccessAction` type can be extended beyond the basic CRUD operations ("Create", "Update", "Delete", "View") to include custom actions specific to your application needs. This works in both static and dynamic authentication modes. :::info When defining custom actions, note that the standard base actions ("Create", "Update", "Delete", "View") must use Pascal case (capital first letter). Your custom actions can use any naming convention, though we recommend maintaining consistency in your codebase. ::: **Example of custom actions:** ```ts // Custom export functionality with specific access controls router.get( "/api/reports/export", authService.authenticate, authService.handleAccessControl( "Export", // Custom action in Pascal case "report", ["Admin", "Analyst"] // accessControl ), exportReportController ); // Custom bulk operation with specific permissions router.post( "/api/users/bulk-invite", authService.authenticate, authService.handleAccessControl( "BulkInvite", // Custom action in Pascal case "user", ["Admin", "HR"] // accessControl ), bulkInviteController ); ``` :::tip For more detailed information on implementing authentication in custom routers, see the guide on [Adding Authentication to Custom Routers](/docs/core-concepts/routing/setup#adding-authentication-to-custom-routers). ::: ## Type Reference [#type-reference] This section provides reference information for the TypeScript types used with the auth service. ### `AccessAction` [#accessaction] Represents the possible actions that can be performed by a controller, including standard CRUD operations and custom actions. ```ts export type AccessAction = "Create" | "Update" | "Delete" | "View" | string; ``` ### `AccessControlRules` [#accesscontrolrules] Defines access control rules for different controller actions. Each key maps to an array of role names that are allowed to perform the action. ```ts export type AccessControlRules = { [key in AccessAction]: string[]; }; ``` ### `AuthenticationControlRules` [#authenticationcontrolrules] Specifies which actions require authentication. ```ts export type AuthenticationControlRules = { [key in AccessAction]: boolean; }; ``` ### `AuthenticationControlConfig` [#authenticationcontrolconfig] Configuration for authentication control. Can be a boolean (applies to all actions) or specific rules per action. ```ts export type AuthenticationControlConfig = | boolean | Partial; ``` ### `AccessControlConfig` [#accesscontrolconfig] Configuration for access control. Can be an array of roles (applies to all actions) or specific rules per action. ```ts export type AccessControlConfig = string[] | Partial; ``` ### `AuthConfigs` [#authconfigs] Configuration for authentication and access control. ```ts export type AuthConfigs = { authenticationControl?: AuthenticationControlConfig; accessControl?: AccessControlConfig; }; ``` ### `AuthJwtPayload` [#authjwtpayload] Payload structure for JWT-based authentication, extending the standard `JwtPayload`. ```ts export interface AuthJwtPayload extends JwtPayload { id?: number | string; [x: string]: any; } ``` ## Best Practices [#best-practices] 1. **Use What Is Needed**: Try to avoid rewriting things that **Arkos** do behind the scenes unless you really now what you want to do. 2. **Custom Authentication Flows**: When implementing specialized authentication like SSO or multi-factor authentication, use the built-in authService methods as building blocks rather than reimplementing core logic. 3. **Security Context**: Always maintain the security context through the request pipeline. Avoid storing sensitive information in request objects that will be exposed to the client. 4. **Graceful Degradation**: When implementing optional authentication, ensure your application gracefully handles unauthenticated scenarios rather than failing completely. 5. **Role-Based Policies**: Use the access control handlers to create fine-grained permission policies based on roles and resource ownership rather than hardcoding permissions checks. 6. **Error Standardization**: Follow Arkos error handling patterns by using AppError with appropriate status codes to maintain consistent API responses for authentication failures. # Base Controller The `BaseController` class provides standardized RESTful API endpoints for any given prisma model in your application. It follows a standard controller pattern that handles common CRUD operations through a consistent interface, reducing code duplication across the application. :::note By default it is an **Arkos** internal utility class for handling the auto generated api endpoints, but this is exposed through `arkos` so that you can customize the end handler of your CRUD operations. ::: As stated on the note above this is a class used internally by **Arkos** and also exposed for you if you want to override and customize the end handler. Hence the next texts is about how **Arkos** uses it behind the scenes to handle the auto generated endpoints. ### Purpose [#purpose] This class provides a reusable set of controller methods that can be used for any model in the system, implementing standard REST operations. ### Constructor [#constructor] ```ts constructor(modelName: string) ``` * **Parameters**: * `modelName`: The name of the model for which this controller will handle operations. * **Behavior**: * Initializes a new `BaseService` instance for the specified model, [read more here](/docs/reference/base-service). * Loads any model-specific interceptor middlewares from the model modules, [see more here](/docs/core-concepts/components/interceptors). ### Methods [#methods] #### **createOne** [#createone] Creates a single resource instance. * **HTTP Method**: `POST` * **Response**: * Status Code: `201 Created` * Body: `{ data: }` * **Interceptor Middleware Support**: * Can use `beforeCreateOne` middleware for pre-processing. * Can use `afterCreateOne` middleware for post-processing. #### **createMany** [#createmany] Creates multiple resource instances in a single operation. * **HTTP Method**: `POST` * **Response**: * Status Code: `201 Created` * Body: `{ total: , results: , data: }` * **Interceptor Middleware Support**: * Can use `beforeCreateMany` middleware for pre-processing. * Can use `afterCreateMany` middleware for post-processing. #### **findMany** [#findmany] Retrieves multiple resources with filtering, sorting, pagination, and field selection. * **HTTP Method**: `GET` * **Query Parameters**: * Supports filtering, sorting, field limiting, and pagination via `APIFeatures`. * **Response**: * Status Code: `200 OK` * Body: `{ total: , results: , data: }` * **Interceptor Middleware Support**: * Can use `beforeFindMany` middleware for pre-processing. * Can use `afterFindMany` middleware for post-processing. #### **findOne** [#findone] Retrieves a single resource by its identifier. * **HTTP Method**: `GET` * **URL Parameters**: * `id`: The ID of the resource to retrieve. * **Query Parameters**: * `prismaQueryOptions`: Optional Prisma query options as a string. * **Response**: * Status Code: `200 OK` * Body: `{ data: }` * **Interceptor Middleware Support**: * Can use `beforeFindOne` middleware for pre-processing. * Can use `afterFindOne` middleware for post-processing. #### **updateOne** [#updateone] Updates a single resource by its identifier. * **HTTP Method**: `PUT` or `PATCH` * **URL Parameters**: * `id`: The ID of the resource to update. * **Query Parameters**: * `prismaQueryOptions`: Optional Prisma query options as a string. * **Response**: * Status Code: `200 OK` * Body: `{ data: }` * **Interceptor Middleware Support**: * Can use `beforeUpdateOne` middleware for pre-processing. * Can use `afterUpdateOne` middleware for post-processing. #### **updateMany** [#updatemany] Updates multiple resources that match the given criteria. * **HTTP Method**: `PUT` or `PATCH` * **Query Parameters**: * Requires at least one filter criterion. * Supports filtering and sorting via `APIFeatures`. * **Response**: * Status Code: `200 OK` * Body: `{ total: , results: , data: }` * **Interceptor Middleware Support**: * Can use `beforeUpdateMany` middleware for pre-processing. * Can use `afterUpdateMany` middleware for post-processing. * **Error Handling**: Returns `400 Bad Request` if no filter criteria are provided. #### **deleteOne** [#deleteone] Deletes a single resource by its identifier. * **HTTP Method**: `DELETE` * **URL Parameters**: * `id`: The ID of the resource to delete. * **Response**: * Status Code: `204 No Content` * **Interceptor Middleware Support**: * Can use `beforeDeleteOne` middleware for pre-processing. * Can use `afterDeleteOne` middleware for post-processing. #### **deleteMany** [#deletemany] Deletes multiple resources that match the given criteria. * **HTTP Method**: `DELETE` * **Query Parameters**: * Requires at least one filter criterion. * Supports filtering and sorting via `APIFeatures`. * **Response**: * Status Code: `200 OK` * Body: `{ total: , results: , data: }` * **Interceptor Middleware Support**: * Can use `beforeDeleteMany` middleware for pre-processing. * Can use `afterDeleteMany` middleware for post-processing. * **Error Handling**: Returns `400 Bad Request` if no filter criteria are provided. ## Default Endpoints When Auto Generated [#default-endpoints-when-auto-generated] **Format**: `/api/[pluralized-model-name]` * For bulk operations (createMany, updateMany, deleteMany), append /many. * For single operations, use the base path and optionally /:id for operations requiring a resource ID. ## Function: getAvalibleRoutes [#function-getavalibleroutes] ### Purpose [#purpose-1] Returns a list of all registered API routes in the Express application. ### Parameters [#parameters] * `req`: The Express request object. * `res`: The Express response object. * `next`: The Express next function. ### Response [#response] * A JSON array containing objects with `method` and `path` properties for each registered route. ## Function: getAvailableResources [#function-getavailableresources] ### Purpose [#purpose-2] Returns a list of all available resource endpoints based on the application's models. ### Response [#response-1] * Status Code: 200 (OK) * Body: `{ data: }` ## Overriding Default Handlers [#overriding-default-handlers] #### Creating a Controller for a Specific Model [#creating-a-controller-for-a-specific-model] ```ts // src/modules/user/user.controller.ts import { ArkosRequest, ArkosResponse, ArkosNextFunction, BaseController, } from "arkos"; class UserController extends BaseController { constructor() { super("user"); // model-name in kebab-case // Add any user-specific controller methods or override base methods here // ✅ Will override the default createOne handler method createOne: catchAsync( async ( req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction ) => { // this.service is made available on `BaseController` const data = await this.service.createOne(req.body, { include: { password: false, }, }); res.status(201).json({ data }); } ); } } const userController = new UserController(); export default userController; ``` :::danger caution Be carreful when overriding the defaults controller handlers as shown above, just do it if you know what you are really doing. Because this way you lose some **Arkos** built-in features such as (depending on the overridden handler): `request query paramaters handling`, `auto relation fields handling`, `standardized responses it all endpoints`, `must create your swagger docs for the endpoints`, `intercepetor middlewares injections` and others. ::: When using `before` or `after` interceptor middlewares defined in the model's module file, they will be automatically loaded and executed after the corresponding operation but before sending the response even for `after` interceptor middlewares, there you can call `next()` and **Arkos** will send the responpse or you can send it youself using the `res` object. You can read more about those interceptor middlewares [here](/docs/core-concepts/components/interceptors). # Base Service The `BaseService` class is a fundamental component that provides standardized CRUD (Create, Read, Update, Delete) operations for all models in your application. It serves as the foundation for **Arkos**'s Prisma integration and can be extended for model-specific implementations. ## Key Features [#key-features] * Provide consistent, reusable data access methods across all models * Handle common operations like relation management and error handling * Support for [Service Hooks](/docs/core-concepts/components/service-hooks) to execute custom logic * Allow for model-specific overrides and extensions * Reduce code duplication in service implementations * Full TypeScript support with automatic type inference (v1.4.0+) ## TypeScript Integration [#typescript-integration] ### Enhanced Type Safety [#enhanced-type-safety] Starting with v1.4.0, Arkos provides **automatic type inference** for all BaseService methods when you run the type generation command: ```bash npx arkos prisma generate ``` This command does two things: 1. Runs `npx prisma generate` to generate your Prisma client 2. Generates enhanced TypeScript definitions that sync with your Prisma schema **What gets generated:** The command creates type definitions in `node_modules/arkos/types/modules/base/base.service.d.ts` that map your Prisma models to fully typed BaseService methods. ### Creating Type-Safe Services [#creating-type-safe-services] ```typescript // src/modules/user/user.service.ts import { BaseService } from "arkos/services"; // ✅ Full type inference after running `npx arkos prisma generate` class UserService extends BaseService<"user"> { // TypeScript knows all available fields and relations async findActiveUsers() { // Full autocomplete for query options return this.findMany( { status: "ACTIVE" }, { include: { profile: true, posts: { where: { published: true }, take: 10, }, }, orderBy: { createdAt: "desc" }, } ); } } const userService = new UserService("user"); export default userService; ``` **Benefits:** * ✅ Full autocomplete for all fields and relations * ✅ Type checking for query options * ✅ Compile-time validation of includes, selects, and where clauses * ✅ Return types automatically inferred based on your query options ### Basic TypeScript Support (v1.3.0) [#basic-typescript-support-v130] In v1.3.0, you need to manually specify the Prisma delegate type and even with there was not a good type inference: ```typescript // src/modules/user/user.service.ts import { BaseService } from "arkos/service"; import { Prisma } from "@prisma/client"; class UserService extends BaseService { async findActiveUsers() { return this.findMany( { status: "ACTIVE" }, { include: { profile: true, posts: true, }, } ); } } const userService = new UserService("user"); export default userService; ``` :::tip Upgrade Recommendation We highly recommend upgrading to v1.4.0-beta for enhanced TypeScript support. The improved type inference significantly reduces development time and prevents runtime errors. ::: ## Constructor [#constructor] ```typescript constructor(modelName: string) ``` Creates a new BaseService instance for the specified model. **Parameters:** * `modelName`: The kebab-case name of your Prisma model **Example:** ```typescript import { BaseService } from "arkos/services"; const userProfileService = new BaseService("user-profile"); ``` ## Properties [#properties] | Property | Type | Description | | ---------------- | -------------------------- | --------------------------------------------------- | | `modelName` | `string` | The kebab-case name of the model | | `relationFields` | `ModelGroupRelationFields` | Object containing singular and list relation fields | | `prisma` | `PrismaClient` | Instance of the Prisma client | ## Core CRUD Methods [#core-crud-methods] ### `createOne` [#createone] Creates a single record in the database. ```typescript async createOne( data: CreateOneData, queryOptions?: TOptions, context?: ServiceBaseContext ): Promise> ``` **Parameters:** * `data`: Object containing data for the new record * `queryOptions`: (Optional) Additional Prisma query options (include, select, etc.) * `context`: (Optional) Context object with user info and execution options **Returns:** The created record with applied query options **Special Handling:** * Automatically hashes passwords for User model * Handles relation fields (connect, create, connectOrCreate) * Executes [before/after/error hooks](/docs/core-concepts/components/service-hooks) **Example:** ```typescript // Basic creation const user = await userService.createOne({ email: "user@example.com", name: "John Doe", password: "securepassword", // Auto-hashed for User model }); // With relations const post = await postService.createOne( { title: "My First Post", content: "Post content here", author: { connect: { id: userId }, }, tags: { create: [{ name: "javascript" }, { name: "typescript" }], }, }, { include: { author: true, tags: true, }, } ); // With user context (for hooks) const product = await productService.createOne( { name: "Laptop", price: 999.99 }, { include: { category: true } }, { user: currentUser, accessToken: req.headers.authorization } ); ``` ### `createMany` [#createmany] Creates multiple records in a single database operation. ```typescript async createMany( data: CreateManyData, queryOptions?: TOptions, context?: ServiceBaseContext ): Promise> ``` **Parameters:** * `data`: Array of objects containing data for the new records * `queryOptions`: (Optional) Additional Prisma query options * `context`: (Optional) Context object **Returns:** Object with count and created records **Special Handling:** * Automatically hashes passwords for User model (for each user in array) * Handles relation fields for each record * Executes hooks for the batch operation **Example:** ```typescript // Create multiple users const result = await userService.createMany([ { email: "user1@example.com", name: "User 1", password: "pass1" }, { email: "user2@example.com", name: "User 2", password: "pass2" }, { email: "user3@example.com", name: "User 3", password: "pass3" }, ]); console.log(result.count); // 3 // With relations const posts = await postService.createMany( [ { title: "Post 1", content: "Content 1", author: { connect: { id: userId } }, }, { title: "Post 2", content: "Content 2", author: { connect: { id: userId } }, }, ], { include: { author: true }, } ); ``` ### `findMany` [#findmany] Retrieves multiple records based on provided filters. ```typescript async findMany( filters?: FindManyFilters, queryOptions?: TOptions, context?: ServiceBaseContext ): Promise> ``` **Parameters:** * `filters`: (Optional) Object containing filters to apply * `queryOptions`: (Optional) Pagination, sorting, includes, etc. * `context`: (Optional) Context object **Returns:** Array of found records **Special Handling:** * By default includes singular relation fields (for performance) * Supports all Prisma filter operations (where, orderBy, take, skip, etc.) **Example:** ```typescript // Find all active users const users = await userService.findMany({ status: "ACTIVE", }); // With pagination and sorting const posts = await postService.findMany( { published: true, author: { status: "ACTIVE", }, }, { take: 10, skip: 0, orderBy: { createdAt: "desc" }, include: { author: true, comments: { take: 5, orderBy: { createdAt: "desc" }, }, }, } ); // Complex filtering const products = await productService.findMany( { OR: [{ category: "Electronics" }, { featured: true }], price: { gte: 100, lte: 1000, }, }, { orderBy: [{ featured: "desc" }, { price: "asc" }], } ); ``` ### `findById` [#findbyid] Finds a single record by its ID. ```typescript async findById( id: string | number, queryOptions?: TOptions, context?: ServiceBaseContext ): Promise | null> ``` **Parameters:** * `id`: The record ID (string or number) * `queryOptions`: (Optional) Additional query options * `context`: (Optional) Context object **Returns:** The found record or `null` if not found **Example:** ```typescript // Find by ID const user = await userService.findById("user-uuid-123"); // With relations const post = await postService.findById("post-id-456", { include: { author: true, comments: true, tags: true, }, }); if (!post) { throw new Error("Post not found"); } ``` ### `findOne` [#findone] Finds a single record by custom filters. ```typescript async findOne( filters: FindOneFilters, queryOptions?: TOptions, context?: ServiceBaseContext ): Promise | null> ``` **Parameters:** * `filters`: Object containing criteria to find the record * `queryOptions`: (Optional) Additional query options * `context`: (Optional) Context object **Returns:** The found record or `null` if not found **Special Handling:** * Uses `findUnique` when filtering by ID only (more performant) * Uses `findFirst` for other filters * Includes all relation fields by default **Example:** ```typescript // Find by unique field const user = await userService.findOne({ email: "user@example.com", }); // Find with complex filters const post = await postService.findOne( { slug: "my-post-slug", published: true, }, { include: { author: { include: { profile: true, }, }, comments: { where: { approved: true }, take: 10, }, }, } ); // Using ID (automatically uses findUnique) const product = await productService.findOne({ id: "product-123" }); ``` ### `updateOne` [#updateone] Updates a single record by its filters. ```typescript async updateOne( filters: UpdateOneFilters, data: UpdateOneData, queryOptions?: TOptions, context?: ServiceBaseContext ): Promise> ``` **Parameters:** * `filters`: Object containing criteria to find the record * `data`: Object containing data to update * `queryOptions`: (Optional) Additional query options * `context`: (Optional) Context object **Returns:** The updated record **Special Handling:** * Automatically hashes passwords for User model * Handles relation fields (connect, disconnect, update) * Executes before/after/error hooks **Example:** ```typescript // Simple update const user = await userService.updateOne({ id: userId }, { name: "New Name" }); // Update with relations const post = await postService.updateOne( { id: postId }, { title: "Updated Title", published: true, tags: { connect: [{ id: "tag1" }, { id: "tag2" }], disconnect: [{ id: "tag3" }], }, }, { include: { author: true, tags: true, }, } ); // Password update (auto-hashed for User) const updatedUser = await userService.updateOne( { email: "user@example.com" }, { password: "newpassword123" } // Automatically hashed ); ``` ### `updateMany` [#updatemany] Updates multiple records based on filters. ```typescript async updateMany( filters: UpdateManyFilters, data: UpdateManyData, queryOptions?: TOptions, context?: ServiceBaseContext ): Promise> ``` **Parameters:** * `filters`: Object containing filters to identify records * `data`: Object containing data to update * `queryOptions`: (Optional) Additional query options * `context`: (Optional) Context object **Returns:** Object with count of updated records **Example:** ```typescript // Update multiple posts const result = await postService.updateMany( { authorId: userId, published: false, }, { published: true, publishedAt: new Date(), } ); console.log(`Published ${result.count} posts`); // Conditional bulk update await productService.updateMany( { category: "Electronics", stock: { lte: 10 }, }, { status: "LowStock", } ); ``` ### `deleteOne` [#deleteone] Deletes a single record by its filters. ```typescript async deleteOne( filters: DeleteOneFilters, context?: ServiceBaseContext ): Promise> ``` **Parameters:** * `filters`: Object containing parameters to find the record * `context`: (Optional) Context object **Returns:** The deleted record **Example:** ```typescript // Delete by ID const deletedPost = await postService.deleteOne({ id: postId }); // Delete by other criteria const deletedUser = await userService.deleteOne({ email: "user@example.com", }); ``` ### `deleteMany` [#deletemany] Deletes multiple records based on filters. ```typescript async deleteMany( filters: DeleteManyFilters, context?: ServiceBaseContext ): Promise> ``` **Parameters:** * `filters`: Object containing filters to identify records * `context`: (Optional) Context object **Returns:** Object with count of deleted records **Example:** ```typescript // Delete old posts const result = await postService.deleteMany({ createdAt: { lt: new Date("2023-01-01"), }, published: false, }); console.log(`Deleted ${result.count} old drafts`); // Delete user's comments await commentService.deleteMany({ authorId: userId, }); ``` ### `count` [#count] Counts records matching the filters. ```typescript async count( filters?: CountFilters, context?: ServiceBaseContext ): Promise ``` **Parameters:** * `filters`: (Optional) Object containing filters * `context`: (Optional) Context object **Returns:** Number of matching records **Example:** ```typescript // Count all users const totalUsers = await userService.count(); // Count active users const activeUsers = await userService.count({ status: "Active", }); // Count published posts by author const publishedCount = await postService.count({ authorId: userId, published: true, }); ``` ## Batch Operations (Transactions) [#batch-operations-transactions] ### `batchUpdate` [#batchupdate] Updates multiple records in a single transaction with individual filters and data. ```typescript async batchUpdate( dataArray: Array & { where: any }>, queryOptions?: TOptions, context?: ServiceBaseContext ): Promise>> ``` **Parameters:** * `dataArray`: Array of objects containing `where` filters and update data * `queryOptions`: (Optional) Query options applied to all updates * `context`: (Optional) Context object **Returns:** Array of updated records **Example:** ```typescript // Update multiple posts with different data const updated = await postService.batchUpdate([ { where: { id: "post1" }, title: "Updated Title 1", published: true, }, { where: { id: "post2" }, title: "Updated Title 2", content: "New content", }, { where: { id: "post3" }, featured: true, }, ]); // All operations succeed or all fail (transaction) ``` ### `batchDelete` [#batchdelete] Deletes multiple specific records in a single transaction. ```typescript async batchDelete( batchFilters: Array>, context?: ServiceBaseContext ): Promise>> ``` **Parameters:** * `batchFilters`: Array of filter objects to identify records * `context`: (Optional) Context object **Returns:** Array of deleted records **Example:** ```typescript // Delete specific posts by ID const deleted = await postService.batchDelete([ { id: "post1" }, { id: "post2" }, { id: "post3" }, ]); // Delete by different criteria await commentService.batchDelete([ { id: "comment1" }, { authorId: userId, status: "SPAM" }, { id: "comment3" }, ]); ``` ## Service Context [#service-context] The `ServiceBaseContext` object allows you to pass request-specific information to service methods and control hook execution: ```typescript interface ServiceBaseContext { user?: User; // Authenticated user accessToken?: string; // Access token from request skip?: | "before" | "after" | "error" | "all" | Array<"before" | "after" | "error">; // Skip specific hooks throwOnError?: boolean; // Whether to throw errors (default: true) } ``` **Example:** ```typescript // Pass user context (available in hooks) const post = await postService.createOne( { title: "My Post", content: "..." }, { include: { author: true } }, { user: req.user, accessToken: req.headers.authorization, } ); // Skip after hooks for performance const data = await userService.findMany( { status: "Active" }, {}, { skip: "after" } ); // Skip all hooks const rawData = await postService.findOne({ id: postId }, {}, { skip: "all" }); // Don't throw errors, return undefined instead const result = await userService.createOne( invalidData, {}, { throwOnError: false } ); if (!result) { console.log("Creation failed but didn't throw"); } ``` ## Extending BaseService [#extending-baseservice] ### File Structure [#file-structure] ``` my-arkos-project/ └── src/ └── modules/ └── [model-name]/ ├── [model-name].service.ts ← Custom service ├── [model-name].hooks.ts ← Service hooks ├── [model-name].interceptors.ts ← HTTP interceptors (v1.4.0+) └── [model-name].middlewares.ts ← HTTP interceptors (v1.3.0) ``` ### Generating Custom Services [#generating-custom-services] Use the Arkos CLI to scaffold service files: ```bash npx arkos generate service --module post ``` **Shorthand:** ```bash npx arkos g s -m post ``` ### Example: Custom User Service [#example-custom-user-service] ```typescript // src/modules/user/user.service.ts import { BaseService } from "arkos/services"; import { AppError } from "arkos/error-handler"; import authService from "../auth/auth.service"; import emailService from "../email/email.service"; class UserService extends BaseService<"user"> { // Custom method: Find by email async findByEmail(email: string) { return this.findOne( { email }, { include: { profile: true, posts: { where: { published: true }, take: 10, }, }, } ); } // Custom method: Change password async changePassword( userId: string, oldPassword: string, newPassword: string ) { const user = await this.findById(userId); if (!user) { throw new AppError("User not found", 404); } // Validate old password const isValid = await authService.isCorrectPassword( oldPassword, user.password ); if (!isValid) { throw new AppError("Invalid old password", 400); } // Update password (automatically hashed by BaseService) return this.updateOne({ id: userId }, { password: newPassword }); } // Custom method: Get user statistics async getUserStats(userId: string) { const [user, postCount, commentCount] = await Promise.all([ this.findById(userId), this.prisma.post.count({ where: { authorId: userId } }), this.prisma.comment.count({ where: { authorId: userId } }), ]); return { user, stats: { posts: postCount, comments: commentCount, joinedAt: user?.createdAt, }, }; } } // Export as singleton const userService = new UserService("user"); export default userService; ``` You can then use [**Service Hooks**](/docs/core-concepts/components/service-hooks) to customize every|some calls of `createOne` method: ```ts // src/modules/user/user.hooks.ts import { BeforeCreateOneHookArgs, AfterCreateOneHookArgs, } from "arkos/services"; import { Prisma } from "@prisma/client"; import { AppError } from "arkos/error-handler"; import userService from "./user.service"; import emailService from "../email/email.service"; export const beforeCreateOne = [ async ({ data, queryOptions, context, }: BeforeCreateOneHookArgs) => { if (!data.email) throw new AppError("Email is required", 400); const existing = await userService.findOne({ email: data.email }); if (existing) throw new AppError("Email already in use", 400); }, ]; export const afterCreateOne = [ async ({ result, data, queryOptions, context, }: AfterCreateOneHookArgs) => { await emailService.sendWelcomeEmail({ to: result.email, name: result.name, }); }, ]; ``` ## Best Practices [#best-practices] ### 1. Keep Constructor Simple [#1-keep-constructor-simple] ```typescript class ProductService extends BaseService<"product"> { constructor() { super("product"); // Avoid complex initialization here } } ``` ### 2. Reuse Parent Methods [#2-reuse-parent-methods] ```typescript class PostService extends BaseService<"post"> { async createDraft(data: any) { // Call parent with additional data return this.createOne({ ...data, published: false, publishedAt: null, }); } } ``` ### 3. Use Service Hooks for Business Logic [#3-use-service-hooks-for-business-logic] Instead of overriding methods, use hooks when possible: ```typescript // ✅ Better: Use hooks (post.hooks.ts) export const beforeCreateOne = [ async ({ data, context }) => { if (!data.slug) data.slug = generateSlug(data.title); }, ]; ``` ### 4. Handle Transactions Properly [#4-handle-transactions-properly] ```typescript class OrderService extends BaseService<"order"> { async createOrderWithItems(orderData: any, items: any[]) { return this.prisma.$transaction(async (tx) => { // Create order const order = await tx.order.create({ data: orderData, }); // Create order items await tx.orderItem.createMany({ data: items.map((item) => ({ ...item, orderId: order.id, })), }); return order; }); } } ``` ### 5. Export as Singleton [#5-export-as-singleton] ```typescript class UserService extends BaseService<"user"> {} // ✅ Export singleton instance const userService = new UserService("user"); export default userService; // ❌ Don't export the class // export default UserService; ``` ## Common Patterns [#common-patterns] ### 1. Soft Deletes [#1-soft-deletes] ```typescript class PostService extends BaseService<"post"> { async softDelete(id: string) { return this.updateOne( { id }, { deleted: true, deletedAt: new Date(), } ); } async findActive(filters: any = {}) { return this.findMany({ ...filters, deleted: false, }); } } ``` ### 2. Pagination Helper [#2-pagination-helper] ```typescript class ProductService extends BaseService<"product"> { async paginate(page: number = 1, limit: number = 10, filters: any = {}) { const skip = (page - 1) * limit; const [items, total] = await Promise.all([ this.findMany(filters, { take: limit, skip }), this.count(filters), ]); return { items, pagination: { page, limit, total, pages: Math.ceil(total / limit), }, }; } } ``` ### 3. Search Implementation [#3-search-implementation] ```typescript class PostService extends BaseService<"post"> { async search(query: string, filters: any = {}) { return this.findMany( { ...filters, OR: [ { title: { contains: query, mode: "insensitive" } }, { content: { contains: query, mode: "insensitive" } }, { author: { name: { contains: query, mode: "insensitive" }, }, }, ], }, { include: { author: true, tags: true, }, } ); } } ``` ## Related Documentation [#related-documentation] * **[Service Hooks](/docs/core-concepts/components/service-hooks)** - Execute custom logic during CRUD operations * **[Interceptor Middlewares](/docs/core-concepts/components/interceptors)** - HTTP-level request/response processing * **[Adding Custom Routers](/docs/core-concepts/routing/setup)** - Create custom API endpoints * **[Request Handling Pipeline](/docs/reference/request-handling-pipeline)** - Understand how requests flow through Arkos ## Migration from v1.3.0 to v1.4.0 [#migration-from-v130-to-v140] ### Type Changes [#type-changes] ```typescript import { BaseService } from "arkos/service"; import { Prisma } from "@prisma/client"; class UserService extends BaseService { constructor() { super("user"); } } ``` ```typescript import { BaseService } from "arkos/services"; class UserService extends BaseService<"user"> { constructor() { super("user"); } } // Run this after updating your schema // npx arkos prisma generate ``` ### Key Changes [#key-changes] 2. **Generic type**: `Prisma.UserDelegate` → `"user"` (model name as string literal) 3. **Type generation**: Run `npx arkos prisma generate` after schema changes ### Benefits of Upgrading [#benefits-of-upgrading] * Better autocomplete for all Prisma operations * Compile-time validation of includes and selects * Automatic return type inference based on query options * Reduced type annotations needed * Better IDE support and developer experience :::tip After upgrading to v1.4.0, run `npx arkos prisma generate` to generate the enhanced type definitions. This command should be run whenever you modify your Prisma schema. ::: # Catch Async The `catchAsync` function is a utility function in the **Arkos** that wraps asynchronous request handlers and middleware to automatically catch errors and forward them to Express's error handling mechanism. This eliminates the need for repetitive try-catch blocks in every route handler, creating cleaner code and ensuring consistent error handling. :::tip You can use the function even to catch non async errors by simply letting the function throw it. ::: ## Purpose [#purpose] The `catchAsync` function serves several important purposes: 1. **Error Propagation**: Automatically forwards errors to Arkos's global error handler (built on top of Express global erro handler). 2. **Code Cleanliness**: Eliminates repetitive try-catch blocks in route handlers 3. **Preventing Unhandled Rejections**: Ensures all Promise rejections are properly caught 4. **Centralized Error Handling**: Works with `AppError` to create a cohesive error management system, [read more about AppError](/docs/reference/app-error). 5. **Developer Experience**: Reduces boilerplate code and potential for human error As you are reading about the `catchAsync` maybe you may want to also read about the **Arkos Global Error Handler** [clicking here](/docs/guides/error-handling/overview). ## Function Signature [#function-signature] ```ts const catchAsync = (fn: ArkosRequestHandler) => async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { try { await fn(req, res, next); } catch (err) { next(err); } }; ``` ## Parameters [#parameters] | Parameter | Type | Description | | --------- | --------------------- | -------------------------------------------------------------------------- | | `fn` | `ArkosRequestHandler` | The async Express/Arkos route handler or middleware function to be wrapped | ## Return Value [#return-value] Returns a new async function that: 1. Takes the ArkosRequest, ArkosResponse and ArkosNextFunction extended from Express parameters (`req`, `res`, `next`) 2. Calls the original function within a try-catch block 3. Forwards any caught errors to Arkos's error handling middleware using `next(err)` ## Usage Examples [#usage-examples] ### Basic Route Handler [#basic-route-handler] ```ts import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; import { catchAsync } from "arkos/error-handler"; import { prisma } from "../../utils/prisma"; // Without try-catch boilerplate export const getAllUsers = catchAsync(async (req, res, next) => { const users = await prisma.user.findMany(); res.status(200).json({ status: "success", results: users.length, data: { users }, }); }); ``` As shown below you do not need a try-catch block when using catchAsync, neither forwarding the error to global handler nor even about throwing errors sometimes (Just if you would like a specific message), why??? because **Arkos** handles it for you by providing a set of meaningfull error messages and status code. [read more about](/docs/guides/error-handling/overview) ### With Custom Error Throwing [#with-custom-error-throwing] ```ts import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; import { catchAsync } from "arkos/error-handler"; import { prisma } from "../../utils/prisma"; export const getUserById = catchAsync( async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const user = await prisma.user.findOne({ where: { id: req.params.id }, }); if (!user) { throw new AppError("User not found", 404, { userId: req.params.id, }); } res.status(200).json({ status: "success", data: { user }, }); } ); ``` ### In Middleware [#in-middleware] ```ts import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; import { AppError, catchAsync } from "arkos/error-handler"; export const protectRoute = catchAsync( async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { // Get token from request headers const token = req.headers.authorization?.split(" ")[1]; if (!token) { throw new AppError("Not authenticated. Please log in.", 401); } // Verify token const decoded = await verifyToken(token); // Add user to request object req.user = decoded; // Continue to next middleware/handler next(); } ); ``` :::info Rember that many of the examples mentioned above are alreday handled my **Arkos** behind the scenes for these are basic errors, this way you can focus on what really matters. ::: ## Error Handling Flow [#error-handling-flow] When using `catchAsync`, the error handling flow works like this: 1. Your route handler or middleware executes inside the try block 2. If an error occurs (throw or Promise rejection), it's caught automatically 3. The error is passed to Express's `next` function 4. Express forwards the error to your global error handling middleware 5. The global error handler processes the error (ideally checking for `AppError` instances) ## Benefits [#benefits] ### Why Use catchAsync? [#why-use-catchasync] 1. **DRY Principle**: Eliminates repetitive try-catch blocks across your codebase 2. **Reliability**: Ensures no async errors are missed or unhandled 3. **Consistency**: All errors are channeled through the same error handling process 4. **Readability**: Makes route handlers cleaner and focused on business logic 5. **Maintainability**: Centralizes error handling logic in one place ## Integration with AppError [#integration-with-apperror] `catchAsync` works best when paired with the `AppError` class, [read more](/docs/reference/app-error) about the `AppError` class: ```typescript import { AppError, catchAsync } from "arkos/error-handler"; import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; // Controller function export const updateUser = catchAsync( async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { // Validation if (!req.body.name && !req.body.email) { throw new AppError("Please provide name or email to update", 400); } // Business logic const updatedUser = await User.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true, }); // Resource check if (!updatedUser) { throw new AppError("User not found", 404, { userId: req.params.id, }); } // Response res.status(200).json({ status: "success", data: { user: updatedUser }, }); } ); ``` ## Best Practices [#best-practices] 1. **Wrap All Async Handlers**: Use `catchAsync` for all async route handlers and middleware 2. **Paired with AppError**: Throw `AppError` instances inside your wrapped functions 3. **Express Setup**: Make sure you have a global error handler middleware registered 4. **Error Specificity**: Throw specific errors with appropriate status codes 5. **Clean Architecture**: Consider creating service layers wrapped with their own error handling # Email Service This guide provides a detailed API reference for the `EmailService` class inside **Arkos**, which handles all email functionality in the system. Notice that this is a class used by **Arkos** under the hood and unless you really need to create your own instance and know what you doing you can create your own instance. If you would like to know how to send emails is **Arkos** without the need to create an instance of `EmailService` on your own see [Sending Emails Guide](/docs/guides/email-service). ## Constructor [#constructor] ```ts constructor(config?: SMTPConnectionOptions) ``` Creates a new instance of the `EmailService` class. ### Parameters [#parameters] * `config` (SMTPConnectionOptions, optional): Optional custom SMTP configuration. If provided, these settings will be used instead of the Arkos config. ### Default Configuration [#default-configuration] EmailService by default uses the configuration from `arkos.init()`: ```ts arkos.init({ // other configs email: { host: "smtp.provider.com", port: 465, // Default is 465 secure: true, // Default is true auth: { user: "your@email.com", pass: "yourPassword", }, name: "Company Name", // Optional }, }); ``` ### Example [#example] ```ts // Create using Arkos config from arkos.init() const defaultEmailService = new EmailService(); // Create with custom configuration const customEmailService = new EmailService({ host: "smtp.custom-provider.com", port: 587, secure: false, auth: { user: "custom@example.com", pass: "customPassword" }, name: "Custom Service", }); ``` ## Methods [#methods] ### `send()` [#send] ```ts public async send( options: EmailOptions, connectionOptions?: SMTPConnectionOptions, skipVerification = false ): Promise<{ success: boolean; messageId?: string }> ``` Sends an email with the provided options using either the default configuration or custom connection settings. #### Parameters [#parameters-1] * `options` (EmailOptions): The email content and recipient information. * `connectionOptions` (SMTPConnectionOptions, optional): Custom connection settings for this specific email. * `skipVerification` (boolean, optional): Whether to skip connection verification. Default is `false`. #### Returns [#returns] A Promise that resolves to an object containing: * `success` (boolean): Whether the email was sent successfully. * `messageId` (string, optional): The message ID if successful. #### Throws [#throws] * Error: If the email sending process fails or if connection verification fails. * AppError: If email configuration is not set in Arkos config when using default configuration. #### Example [#example-1] ```ts // Send with default configuration await emailService.send({ to: "user@example.com", subject: "Welcome", html: "

Welcome to our service!

", }); // Send with temporary different credentials await emailService.send( { to: "client@example.com", subject: "Invoice", html: "

Your invoice is ready

", }, { host: "smtp.different-provider.com", auth: { user: "billing@example.com", pass: "billingPass" }, } ); // Skip connection verification (useful for already verified connections) await emailService.send( { to: "quick@example.com", subject: "Quick Message", html: "

This message bypasses verification

", }, undefined, true ); ``` ### `verifyConnection()` [#verifyconnection] ```ts public async verifyConnection(transporterToVerify?: Transporter): Promise ``` Verifies the connection to the email server. #### Parameters [#parameters-2] * `transporterToVerify` (Transporter, optional): A specific transporter to verify. If not provided, verifies the default transporter. #### Returns [#returns-1] A Promise that resolves to: * `true`: If connection is successful. * `false`: If connection fails. #### Example [#example-2] ```ts // Check if email server connection is working const isConnected = await emailService.verifyConnection(); if (isConnected) { console.log("SMTP connection is working correctly"); } else { console.log("SMTP connection failed - please check your credentials"); } ``` ### `updateConfig()` [#updateconfig] ```ts public updateConfig(config: SMTPConnectionOptions): void ``` Updates the custom configuration for this email service instance. #### Parameters [#parameters-3] * `config` (SMTPConnectionOptions): The new connection options. #### Example [#example-3] ```ts emailService.updateConfig({ host: "smtp.newprovider.com", port: 587, secure: false, auth: { user: "new@example.com", pass: "newPassword" }, name: "Updated Email Service", }); ``` ### `static create()` [#static-create] ```ts public static create(config: SMTPConnectionOptions): EmailService ``` Creates a new instance of EmailService with custom configuration. #### Parameters [#parameters-4] * `config` (SMTPConnectionOptions): The connection options for the new instance. #### Returns [#returns-2] A new `EmailService` instance. #### Example [#example-4] ```ts const marketingEmails = EmailService.create({ host: "smtp.marketing-provider.com", auth: { user: "marketing@example.com", pass: "marketingPass" }, name: "Marketing Communications", }); ``` ## Multiple Email Service Instances [#multiple-email-service-instances] For applications that regularly send emails from different accounts: ```typescript import { EmailService } from "arkos/services"; const marketingEmails = EmailService.create({ host: "smtp.marketing-provider.com", auth: { user: "marketing@example.com", pass: "marketingPass" }, }); const supportEmails = EmailService.create({ host: "smtp.support-provider.com", auth: { user: "support@example.com", pass: "supportPass" }, }); // Now you can use them independently await marketingEmails.send({ to: "customer@example.com", subject: "New Products Available", html: "

Check out our new products!

", }); await supportEmails.send({ to: "customer@example.com", subject: "Your Support Ticket", html: "

Your issue has been resolved.

", }); ``` ## Type Definitions [#type-definitions] ### EmailOptions [#emailoptions] ```ts type EmailOptions = { from?: string; // Sender's email address (optional) to: string | string[]; // Recipient(s) email address subject: string; // Subject of the email text?: string; // Plain text body (optional) html: string; // HTML body }; ``` Defines the options for sending an email. ### SMTPAuthOptions [#smtpauthoptions] ```ts type SMTPAuthOptions = { user: string; // Username or email address pass: string; // Password }; ``` Defines the authentication options for SMTP. ### SMTPConnectionOptions [#smtpconnectionoptions] ```ts type SMTPConnectionOptions = { host?: string; // SMTP host server port?: number; // SMTP port secure?: boolean; // Whether to use SSL/TLS auth?: SMTPAuthOptions; // Authentication credentials name?: string; // Email sender name }; ``` Defines the connection options for SMTP server. ## Error Handling [#error-handling] This example shows various ways to use the email service with proper error handling: ```ts import { emailService, EmailService } from "arkos/services"; import { catchAsync } from "arkos/error-handler"; import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; // Example: this is not a built-in middleware const demonstrateEmailService = catchAsync( async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { try { // 1. Send with default configuration from arkos.init() await emailService.send({ to: "user@example.com", subject: "Welcome", html: "

Welcome to our service!

", }); // 2. Send with temporary different credentials await emailService.send( { to: "client@example.com", subject: "Invoice", html: "

Your invoice is ready

", }, { host: "smtp.billing-provider.com", auth: { user: "billing@example.com", pass: "billingPass" }, } ); // 3. Update config for this instance emailService.updateConfig({ host: "smtp.notifications.com", auth: { user: "notifications@example.com", pass: "notifyPass" }, }); // 4. Send with updated config await emailService.send({ to: "member@example.com", subject: "Notification", html: "

You have a new notification

", }); // 5. Create a dedicated instance const marketingEmailer = EmailService.create({ host: "smtp.marketing-server.com", auth: { user: "marketing@example.com", pass: "marketingPass" }, name: "Marketing Team", }); // 6. Use the dedicated instance await marketingEmailer.send({ to: "prospect@example.com", subject: "Special Offer", html: "

Check out our new products!

", }); res.status(200).json({ message: "All emails sent successfully" }); } catch (error) { next(error); } } ); ``` # File Upload Controller The `fileUploadController` provides direct access to Arkos's file upload functionality, allowing you to integrate file upload operations into custom routes with your own business logic and access control rules. ## Overview [#overview] While Arkos provides built-in file upload endpoints at `/api/uploads/:fileType` by default, the `fileUploadController` allows you to create custom routes with specialized middleware, validation, and access control. This is particularly useful when you need to: * Restrict file uploads to specific users (e.g., users can only update their own avatar) * Add custom business logic before or after file operations * Implement different access control rules than the global file upload permissions * Create specialized endpoints with custom validation ## Import [#import] ```typescript import fileUploadController from "arkos/controllers"; ``` ## Available Methods [#available-methods] The `fileUploadController` exposes three main methods for handling file operations: ### `uploadFile` [#uploadfile] Handles file upload requests with support for image processing and multiple file types. **Method Signature:** ```typescript uploadFile(req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) ``` **Supported File Types:** * `images` - Image files with optional processing (resize, format conversion) * `videos` - Video files * `documents` - Document files * `files` - Any other file type **Query Parameters for Images:** * `format` - Convert image format (e.g., `webp`, `jpg`, `png`) * `width` - Set image width in pixels * `height` - Set image height in pixels * `resizeTo` - Resize to fit within specified pixels (maintains aspect ratio) **Response Format:** ```typescript { success: true, data: string | string[], // URL(s) of uploaded file(s) message: "File uploaded successfully" | "${count} files uploaded successfully" } ``` ### `deleteFile` [#deletefile] Handles file deletion requests by file URL. **Method Signature:** ```typescript deleteFile(req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) ``` **Required Parameters:** * `fileType` - The type of file (`images`, `videos`, `documents`, `files`) * `fileName` - The name of the file to delete **Response Format:** ```typescript { success: true, message: "File deleted successfully" } ``` ### `updateFile` [#updatefile] Handles file update requests by deleting the old file and uploading a new one. **Method Signature:** ```typescript updateFile(req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) ``` **Required Parameters:** * `fileType` - The type of file (`images`, `videos`, `documents`, `files`) * `fileName` - The name of the file to update **Query Parameters:** (Same as `uploadFile` for image processing) **Response Format:** ```typescript { success: true, data: string | string[], // URL(s) of new uploaded file(s) message: "File updated successfully" | "File updated successfully. ${count} new files uploaded" } ``` ## Error Handling [#error-handling] All controller methods use Arkos's `catchAsync` wrapper, which automatically handles errors and passes them to the error handling middleware. For more information about error handling, see the [catchAsync function documentation](/docs/reference/catch-async). Common error scenarios: * Invalid file type (400) * No file uploaded (400) * File not found for deletion/update (404) * File processing errors (passed to error handler) ## Usage Examples [#usage-examples] ### Basic Avatar Upload Route [#basic-avatar-upload-route] Create a custom route that allows users to upload only their own avatar: ```ts // src/modules/user/user.controller.ts import { catchAsync, AppError } from "arkos/error-handler"; import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; import userService from "./user.service"; class UserController extends BaseController { constructor() { super("user"); } // Custom middleware to ensure users can only update their own avatar validateAvatarOwnership = catchAsync( async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const { userId } = req.params; // Ensure user can only update their own avatar if (req.user!.id !== userId) { return next(new AppError("You can only update your own avatar", 403)); } // Set the fileType for the controller req.params.fileType = "images"; next(); } ); addUserAvatarFileNameToRequestParams = catchAsync( async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const user = await userService.findById(req.user!.id); req.params.fileType = "images"; req.params.fileName = user.picture; next(); } ); } const userController = new UserController(); export default userController; ``` ```typescript // src/modules/user/user.router.ts import { Router } from "express"; import { fileUploadController } from "arkos/controllers"; import { authService } from "arkos/services"; import userController from "./user.controller"; const userAvatarRouter = Router(); // Avatar upload route with custom access control userAvatarRouter.post( "/:id/avatar", authService.authenticate, userController.validateAvatarOwnership, fileUploadController.updateFile ); // Avatar upload route with custom access control userAvatarRouter.delete( "/:id/avatar", authService.authenticate, userController.addUserAvatarFileNameToRequestParams, fileUploadController.deleteFile ); export default userAvatarRouter; ``` ### Custom Product Image Upload with Business Logic [#custom-product-image-upload-with-business-logic] ```typescript // src/modules/product/product.router.ts import { Router } from "express"; import { RouterConfig } from "arkos"; import fileUploadController from "arkos/controllers"; import { authService } from "arkos/services"; import { catchAsync, AppError } from "arkos/error-handler"; import { prisma } from "../../utils/prisma"; export const config: RouterConfig = { // Keep auto-generated endpoints }; const router = Router(); // Custom middleware for product image upload const validateProductOwnership = catchAsync(async (req, res, next) => { const { productId } = req.params; // Check if product belongs to the authenticated seller const product = await prisma.product.findFirst({ where: { id: productId, sellerId: req.user.id, }, }); if (!product) { return next(new AppError("Product not found or access denied", 404)); } // Set fileType for the controller req.params.fileType = "images"; next(); }); // Custom product image upload endpoint router.post( "/:productId/images", authService.authenticate, authService.handleAccessControl("Create", "product", { Create: ["Seller", "Admin"], }), validateProductOwnership, fileUploadController.uploadFile ); // Custom product image deletion router.delete( "/:productId/images/:fileName", authService.authenticate, authService.handleAccessControl("Delete", "product", { Delete: ["Seller", "Admin"], }), validateProductOwnership, fileUploadController.deleteFile ); export default router; ``` ### Document Upload with File Type Validation [#document-upload-with-file-type-validation] ```typescript // src/routers/document-upload.router.ts import { Router } from "express"; import fileUploadController from "arkos/controllers"; import { authService } from "arkos/services"; import { catchAsync, AppError } from "arkos/error-handler"; import multer from "multer"; const documentRouter = Router(); // Custom middleware to validate document types const validateDocumentType = catchAsync(async (req, res, next) => { const allowedTypes = [".pdf", ".doc", ".docx", ".txt"]; // This would run after multer processes the file if (req.file) { const fileExtension = path.extname(req.file.originalname).toLowerCase(); if (!allowedTypes.includes(fileExtension)) { return next( new AppError("Only PDF, DOC, DOCX, and TXT files are allowed", 400) ); } } req.params.fileType = "documents"; next(); }); // Restricted document upload documentRouter.post( "/api/documents/legal", authService.authenticate, authService.handleAccessControl("Create", "legal-document", { Create: ["Lawyer", "Admin"], }), validateDocumentType, fileUploadController.uploadFile ); export default documentRouter; ``` ## Access Control Considerations [#access-control-considerations] When using `fileUploadController` in custom routes, you have full control over access permissions. This is particularly useful for: ### User-Specific File Operations [#user-specific-file-operations] * Users updating their own profile pictures * Authors managing their own article images * Sellers managing their product photos ### Role-Based File Restrictions [#role-based-file-restrictions] * Only admins can upload certain document types * Different file size limits for different user roles * Restricted file types based on user permissions ### Business Logic Integration [#business-logic-integration] * Validating file ownership before operations * Custom file naming conventions * Integration with your application's data models ## Integration with Interceptor Middlewares [#integration-with-interceptor-middlewares] You can combine `fileUploadController` with Arkos's interceptor middleware system: ```typescript // src/modules/post/post.middlewares.ts import { getFileUploadServices } from "arkos/services"; import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; import { catchAsync } from "arkos/error-handler"; import { prisma } from "../../utils/prisma"; export const beforeUpdateOne = catchAsync( async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { // Handle image upload if present const imageUrl = await getFileUploadServices().imageUploadService.upload( req, res, { format: "webp", resizeTo: 500, } ); if (imageUrl) { // Store old image URL for cleanup const { image } = await prisma.post.findUnique({ where: { id: req.params.id }, select: { image: true }, }); req.body.image = imageUrl; req.query.ignoredFields = { oldImage: image }; } next(); } ); export const afterUpdateOne = catchAsync( async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { // Clean up old image after successful update if (req.body.image && req.query.ignoredFields?.oldImage) { getFileUploadServices() .imageUploadService.deleteByUrl(req.query.ignoredFields.oldImage) .catch(console.error); } next(); } ); ``` For more information about interceptor middlewares, see the [Interceptor Middlewares Guide](/docs/core-concepts/components/interceptors). ## Authentication & Authorization [#authentication--authorization] The `fileUploadController` does not include built-in authentication or authorization. You must add these manually using Arkos's auth services: ### Static RBAC Example [#static-rbac-example] ```typescript import { authService } from "arkos/services"; router.post( "/custom-upload", authService.authenticate, authService.handleAccessControl("Create", "custom-resource", { Create: ["User", "Admin"], }), fileUploadController.uploadFile ); ``` ### Dynamic RBAC Example [#dynamic-rbac-example] ```typescript import { authService } from "arkos/services"; router.post( "/custom-upload", authService.authenticate, authService.handleAccessControl("Create", "custom-resource"), fileUploadController.uploadFile ); ``` For the default file upload endpoints (`/api/uploads/:fileType`), the resource name is `file-upload`. When creating custom routes, you can use any resource name that fits your application's access control structure. ## Best Practices [#best-practices] 1. **Always Add Authentication**: Custom file upload routes should include proper authentication middleware. 2. **Validate File Ownership**: When allowing file operations, ensure users can only modify files they own or have permission to access. 3. **Use Appropriate Resource Names**: Choose descriptive resource names for access control that reflect the specific use case. 4. **Handle Errors Gracefully**: Implement proper error handling for file operations, especially for deletion and update operations. 5. **Clean Up Resources**: When updating files, ensure old files are properly deleted to prevent storage bloat. 6. **Validate File Types**: Add custom validation for file types when needed, beyond the basic `images`, `videos`, `documents`, `files` categories. ## Related Documentation [#related-documentation] * [File Uploads Guide](/docs/guides/file-handling/file-uploads/setup) - General file upload system overview * [Custom Routers Guide](/docs/core-concepts/routing/setup) - Creating custom routes * [Interceptor Middlewares](/docs/core-concepts/components/interceptors) - Using middleware with file operations * [File Upload Services Function Guide](/docs/reference/file-upload-services-function-guide) - Service layer file operations * [Static RBAC Authentication](/docs/core-concepts/authentication/setup) - Access control setup * [Dynamic RBAC Authentication](/docs/core-concepts/authentication/setup#upgrading-to-dynamic-rbac) - Database-driven permissions # File Upload Services Function The `getFileUploadServices` function provides a centralized way to handle file uploads in your **Arkos** application. It creates and returns specialized file uploader services for different file types (images, videos, documents, and general files), each preconfigured with appropriate size limits, file type validation, and processing capabilities. ## Key Features [#key-features] * **Type-specific uploaders**: Separate services for images, videos, documents, and general files * **Automatic configuration**: Uses your **Arkos** config settings or falls back to sensible defaults * **Image processing**: Built-in image resizing and format conversion via Sharp * **Flexible upload options**: Support for both single and multiple file uploads * **URL generation**: Automatically generates accessible URLs for uploaded files * **File deletion**: Ability to delete files using their URLs ## When to Call `getFileUploadServices` [#when-to-call-getfileuploadservices] Always call `getFileUploadServices` inside a function or route handler, not at the module level. This ensures that your **Arkos** configuration is fully loaded before the services are initialized. ```typescript // ❌ Don't do this at the module level const uploaders = getFileUploadServices(); // May use incomplete config // ✅ Do this inside a function or route handler function handleUpload() { const uploaders = getFileUploadServices(); // Configuration is ready // Use uploaders here } ``` ## Basic Usage [#basic-usage] ### Using in Custom Middlewares [#using-in-custom-middlewares] The recommended approach is to use the uploader services in custom middlewares. This gives you full control over the upload process and integration with your business logic: ```typescript // src/modules/post/post.middlewares.ts import { getFileUploadServices } from "arkos/services"; import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; import { catchAsync } from "arkos/error-handler"; import { prisma } from "../../utils/prisma"; export const beforeUpdateOne = catchAsync( async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { // Uploads the image if it exists in request // NB: always call the getFileUploadServices inside a function not outside const imageUrl = await getFileUploadServices().imageUploadService.upload( req, res, { format: "webp", resizeTo: 500, } ); // Checks if an image was uploaded, if yes attach it to the body if (imageUrl) { req.body.image = imageUrl; const { image } = await prisma.post.findUnique({ where: { id: req.params.id, }, select: { image: true, }, }); // When you want to pass data to afterX middleware you can do this req.query.ignoredFields = { oldImage: image, }; } next(); } ); export const afterUpdateOne = catchAsync( async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { // Checks if a file was uploaded by checking the req.body.image field // If yes delete the old one if (req.body.image) getFileUploadServices() .imageUploadService.deleteFileByUrl(req.query.ignoredFields.oldImage) .catch((err) => { console.log(err); }); next(); } ); ``` ### Using in Custom Routes [#using-in-custom-routes] For direct use in routes: ```typescript import { Router } from "express"; import { getFileUploadServices } from "arkos/services"; import { catchAsync } from "arkos/error-handler"; const router = Router(); router.post( "/upload-avatar", catchAsync(async (req, res, next) => { const { imageUploadService } = getFileUploadServices(); const imageUrl = await imageUploadService.upload(req, res, { format: "webp", resizeTo: 300, }); res.status(200).json({ status: "success", data: { avatarUrl: imageUrl }, }); }) ); export default router; ``` ## Available Services [#available-services] The `getFileUploadServices` function returns an object with four specialized services: ```typescript const { imageUploadService, videoUploadService, documentUploadService, fileUploadService, } = getFileUploadServices(); ``` | Service | Purpose | Default Size Limit | File Types | | ----------------------- | -------------------- | ------------------ | ------------------------------------ | | `imageUploadService` | For image uploads | 15 MB | jpeg, jpg, png, gif, webp, svg, etc. | | `videoUploadService` | For video uploads | 5 GB | mp4, avi, mov, mkv, webm, etc. | | `documentUploadService` | For document uploads | 50 MB | pdf, doc, docx, xls, xlsx, etc. | | `fileUploadService` | For any file type | 5 GB | All file types | ## Upload Methods [#upload-methods] Each service provides methods for handling file uploads: ### Single File Upload [#single-file-upload] ```typescript export const uploadProfilePicture = catchAsync( async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const { imageUploadService } = getFileUploadServices(); // Using the upload method const imageUrl = await imageUploadService.upload(req, res); // Update user profile with the new image URL await prisma.user.update({ where: { id: req.user.id }, data: { profilePicture: imageUrl }, }); res.status(200).json({ status: "success", data: { profilePicture: imageUrl }, }); } ); ``` ### Multiple Files Upload [#multiple-files-upload] ```typescript export const uploadProductGallery = catchAsync( async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const { imageUploadService } = getFileUploadServices(); // Set multiple=true in the query parameters req.query.multiple = "true"; // Upload multiple images const imageUrls = await imageUploadService.upload(req, res); // Update product with the new gallery URLs await prisma.product.update({ where: { id: req.params.id }, data: { gallery: { set: imageUrls } }, }); res.status(200).json({ status: "success", data: { gallery: imageUrls }, }); } ); ``` ### Image Processing Options [#image-processing-options] When uploading images, you can provide additional options for processing: ```typescript export const uploadWithProcessing = catchAsync( async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const { imageUploadService } = getFileUploadServices(); // Upload and process the image const imageUrl = await imageUploadService.upload(req, res, { // Convert to WebP format format: "webp", // Resize to specific dimensions width: 800, height: 600, // Or resize proportionally (maintaining aspect ratio) // resizeTo: 1200 // Resize so the smaller dimension equals this value }); res.status(200).json({ status: "success", data: { photoUrl: imageUrl }, }); } ); ``` ## Middleware-Based Upload Methods [#middleware-based-upload-methods] For more control over the upload process, you can also use the middleware-based methods: ```typescript import { Router } from "express"; import { getFileUploadServices } from "arkos/services"; const router = Router(); router.post( "/upload-document", // This creates middleware that handles the upload but doesn't complete the response (req, res, next) => { const { documentUploadService } = getFileUploadServices(); documentUploadService.handleSingleUpload()(req, res, next); }, (req, res) => { // The file is now available in req.file const fileUrl = `${req.protocol}://${req.get( "host" )}/api/uploads/documents/${req.file.filename}`; res.status(200).json({ status: "success", data: { fileUrl }, }); } ); export default router; ``` ## Deleting Files [#deleting-files] To delete a previously uploaded file: ```typescript export const deleteProfilePicture = catchAsync( async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const { user } = req; // Get the current profile picture URL const { profilePicture } = await prisma.user.findUnique({ where: { id: user.id }, select: { profilePicture: true }, }); if (profilePicture) { // Delete the file const { imageUploadService } = getFileUploadServices(); await imageUploadService.deleteFileByUrl(profilePicture); // Update the user record await prisma.user.update({ where: { id: user.id }, data: { profilePicture: null }, }); } res.status(200).json({ status: "success", message: "Profile picture deleted successfully", }); } ); ``` ## Configuration [#configuration] The uploader services are automatically configured based on your Arkos configuration. You can customize the behavior by setting options in your configuration: ```ts // src/app.ts arkos.init({ fileUpload: { baseUploadDir: "./uploads", // Base directory for uploads baseRoute: "/api/uploads", // Base URL route for accessing files restrictions: { images: { maxCount: 50, // Maximum images per upload maxSize: 1024 * 1024 * 20, // 20 MB supportedFilesRegex: /jpeg|jpg|png|gif|webp/, // Allowed file types }, // Similarly for video, document, and other }, }, // other configs }); ``` ## Error Handling [#error-handling] The uploader services throw appropriate error objects when issues occur. You can use Arkos's `catchAsync` utility to handle these errors: ```typescript import { catchAsync } from "arkos/error-handler"; import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos"; // This is a custom middleware and not an Arkos interceptor middleware export const uploadUserDocument = catchAsync( async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const { documentUploadService } = getFileUploadServices(); const documentUrl = await documentUploadService.upload( req, res, "document" ); res.status(200).json({ status: "success", data: { documentUrl }, }); } ); ``` ## Best Practices [#best-practices] 1. **Always call `getFileUploadServices` inside functions**, not at the module level 2. **Select the right service** for the file type you're handling 3. **Validate files on the client-side** before upload to improve user experience 4. **Set appropriate file size limits** to prevent server overload 5. **Handle errors properly** to provide meaningful feedback to users 6. **Clean up old files** when they're no longer needed 7. **Use middleware patterns** to separate upload logic from business logic ## Advanced: Client-Side Integration Example [#advanced-client-side-integration-example] Here's an example of how to integrate with the uploader services from the frontend: ```javascript // React example with axios async function uploadProfileImage(file) { const formData = new FormData(); formData.append("images", file); // even though it's one image the field must be images // (others: videos, documents, files) try { const response = await axios.post("/api/uploads/images", formData, { headers: { "Content-Type": "multipart/form-data", }, // For image processing options params: { format: "webp", resizeTo: 1200, }, }); return response.data.data; } catch (error) { console.error( "Upload failed:", error.response?.data?.message || error.message ); throw error; } } ``` ## Common Use Cases [#common-use-cases] ### User Profile Pictures [#user-profile-pictures] ```typescript export const updateProfilePicture = catchAsync( async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => { const { imageUploadService } = getFileUploadServices(); // Upload and optimize the profile picture const imageUrl = await imageUploadService.upload(req, res, { format: "webp", resizeTo: 300, // Create a reasonably sized profile picture }); if (imageUrl) { // Get the old profile picture to delete it later const { profilePicture } = await prisma.user.findUnique({ where: { id: req.user.id }, select: { profilePicture: true }, }); // Update the user's profile picture await prisma.user.update({ where: { id: req.user.id }, data: { profilePicture: imageUrl }, }); // Delete the old profile picture if it exists if (profilePicture) { imageUploadService.deleteFileByUrl(profilePicture).catch((err) => { console.error("Failed to delete old profile picture:", err); }); } res.status(200).json({ status: "success", data: { profilePicture: imageUrl }, }); } else { res.status(400).json({ status: "error", message: "No file uploaded", }); } } ); ``` ## Troubleshooting [#troubleshooting] ### Common Issues [#common-issues] 1. **"No file uploaded" error** * Ensure the form has `enctype="multipart/form-data"` * Check that the field name matches what your server expects 2. **"Invalid file type" error** * The file type isn't in the allowed types list * Verify the file extension and MIME type 3. **"File too large" error** * The file exceeds the configured size limit * Adjust the size limits in your configuration 4. **Configuration not being applied** * Make sure you're calling `getFileUploadServices` inside a function * Verify your Arkos configuration is properly set up ### Debug Tips [#debug-tips] If you're having issues, try logging the following: ```typescript console.log("File uploader config:", getArkosConfig().fileUpload); console.log("Uploaded file:", req.file); // For single file uploads console.log("Uploaded files:", req.files); // For multiple file uploads ``` By following this guide, you should be able to effectively use the file uploader services in your **Arkos** application for all your file handling needs. # Request Handling Pipeline In **Arkos**, each incoming request goes through a **modular middleware pipeline** that is dynamically constructed based on the Prisma model and its configuration. This allows you to hook into every stage of the request lifecycle without rewriting boilerplate or logic. This pipeline is automatically applied per route for every registered model and operation (e.g., `createOne`, `findMany`, `deleteMany`, etc.), ensuring that every request is authenticated (if desired), validated, processed, and responded to in a consistent and customizable way. :::note This is an explanation on how **Arkos** handles each prisma model request, this is not something you need to implement by yourself unless you want, but by doing this you would not being levarage **Arkos** and it's core features. ::: ## 1. Route Middleware Structure [#1-route-middleware-structure] Each route in **Arkos** is composed of a **chain of middlewares** that execute sequentially. Here's how the flow is constructed: ```ts // some code here router.post('/api/posts', middleware1, middleware2, ..., finalHandler); ``` Arkos does this programmatically for every route generated based on your prisma models. ### Example: [#example] For a `POST /api/users` request, the flow might look like: 1. `Authentication Middleware` 2. `Access Control Middleware` 3. `Validation & Transformation Middleware` 4. `Query Parsing Middleware` 5. `Pre-handler Middleware` (optional) 6. `Main Handler` 7. `Post-handler Middleware` (optional) 8. `Response Sender` ## 2. Execution Order [#2-execution-order] For every endpoint, **Arkos** evaluates and composes middleware like so: ### 2.1. **Authentication** [#21-authentication] Ensures the request is coming from a valid, authenticated user if request is activated for the current route, [read more](/docs/core-concepts/authentication/setup#using-auth-config-to-customize-endpoint-behavior) about how to customize. ```ts authService.handleAuthenticationControl(...) ``` ### 2.2. **Authorization** [#22-authorization] Checks if the authenticated user has permission to perform the action on the specific model, [read more](/docs/core-concepts/authentication/setup#using-auth-config-to-customize-endpoint-behavior) about how to customize. ```ts authService.handleAccessControl(...) ``` ### 2.3. **Validation & Transformation** [#23-validation--transformation] Validates the request body using either `Zod` or `class-validator` and transforms it accordingly, [read more](/docs/guides/validation/setup) about how to work with validation in **Arkos**. ```ts handleRequestBodyValidationAndTransformation(...) ``` ### 2.4. **Custom Prisma Query Options Injection** [#24-custom-prisma-query-options-injection] Injects your Prisma-specific options like filtering, ordering, pagination, etc., into the request, [read more](/docs/core-concepts/prisma-orm/custom-queries) about how to inject your own custom prisma query options on the generated api routes. ```ts addPrismaQueryOptionsToRequestQuery(...) ``` ### 2.5. **Custom Model-Specific Interceptor Middlewares** [#25-custom-model-specific-interceptor-middlewares] Each model can define `before*` and `after*` middlewares for custom logic. **Arkos** checks and applies them automatically: ```ts middlewares?.beforeCreateOne; middlewares?.afterCreateOne; ``` These interceptors middlewares let you inject logic such as business rules, analytics, or external service calls **before or after** the core handler runs, [read more](/docs/core-concepts/components/interceptors) how to implement this. ### 2.6. **Core Handler** [#26-core-handler] The default database operation (e.g., `createOne`, `findMany`, etc.) is executed by the `BaseController` which you can read more about [clicking here](/docs/reference/base-controller). ```ts createOne, findOne, updateMany, etc. ``` ### 2.7. **sendResponse** [#27-sendresponse] This is the fallback and final middleware. If no custom response is sent earlier, it will format and send the standard Arkos response format. ```ts sendResponse; ``` ## 3. Dynamic Route Assembly [#3-dynamic-route-assembly] All routes are auto-generated from your models. For each model: * Resource paths are automatically pluralized * CRUD operations are scaffolded using the Prisma client * Custom middlewares per model are loaded * Full security, validation, and query parsing pipelines are constructed Example route: ``` POST /api/users; ``` Becomes automatically: ```ts router.post( "/api/users", authService.handleAuthenticationControl(...), authService.handleAccessControl(...), handleRequestBodyValidationAndTransformation(...), addPrismaQueryOptionsToRequestQuery(...), middlewares?.beforeCreateOne ?? createOne, middlewares?.beforeCreateOne ? createOne : middlewares?.afterCreateOne ?? sendResponse, middlewares?.afterCreateOne ?? sendResponse, sendResponse ); ``` This above is an **Arkos** inner implementation if you want to add your own routers, see how to add your own routes endpoints that goes beyond the auto generated by **Arkos**, reading [Adding Custom Routers Guide](/docs/core-concepts/routing/setup). ## 4. Interceptor Middleware For Override & Extension [#4-interceptor-middleware-for-override--extension] You can fully customize the pipeline by defining any of the following interpcetor middlewares in your model module: ```ts // src/modules/[model-name]/[mode-name].middlewares.ts import { catchAsync } from "arkos/error-handler" export const beforeCreateOne = catchAsync(async (req, res, next) => { ... }) export const afterCreateOne = catchAsync(async (req, res, next) => { ... }) ``` See exactly how it works [here](/docs/core-concepts/components/interceptors). ## Summary [#summary] **Arkos** provides a **structured, extensible request handling pipeline** for every route: * Consistent across models * Customizable with interceptor middlewares * Security-first with built-in auth * Integrated with Prisma query features This system enables you to scale your API with confidence while maintaining complete control over every request. # E2E Testing a # Integration Testing a # Setup a # Unit Testing a # Authentication These commands generate the permission and access control files for your modules. In v1.6, the recommended approach is `ArkosPolicy`. The older `auth-configs` command still works but generates a deprecated file format. ## Policy (v1.6+, recommended) [#policy-v16-recommended] ```bash arkos generate policy --module post arkos g p -m post ``` **Output:** `src/modules/post/post.policy.ts` For Prisma model modules, the generator produces a policy with the four standard CRUD rules pre-configured: ```ts import { ArkosPolicy } from "arkos"; const postPolicy = ArkosPolicy("post") .rule("Create", { name: "Create Post", description: "Permission to create new post records", }) .rule("View", { name: "View Post", description: "Permission to view post records", }) .rule("Update", { name: "Update Post", description: "Permission to update existing post records", }) .rule("Delete", { name: "Delete Post", description: "Permission to delete post records", }); export default postPolicy; ``` If your Arkos config has `authentication.mode` set to `"static"`, the generator adds a `roles` array to each rule: ```ts .rule("Create", { roles: [], name: "Create Post", description: "Permission to create new post records", }) ``` For modules that are not Prisma models, a minimal policy with no rules is generated — add your own rules for whatever operations your custom module exposes. Once you have a policy file you can reference it in your `RouteHook` to protect individual operations: ```ts import postPolicy from "@/src/modules/post/post.policy"; export const hook: RouteHook<"prisma"> = { createOne: { authentication: postPolicy.Create }, deleteOne: { authentication: postPolicy.Delete }, } ``` See the [Route Hook](/docs/core-concepts/components/route-hooks) guide for full details on the `authentication` key. ## Auth Configs (deprecated) [#auth-configs-deprecated] ```bash arkos generate auth-configs --module post arkos g a -m post ``` **Output:** `src/modules/post/post.auth.ts` This command generates a file and immediately prints a deprecation warning directing you to migrate to `ArkosPolicy`. The generated file still works but will be removed in v2.0. ```ts import { AuthConfigs } from "arkos/auth"; import { authService } from "arkos/services"; export const postAccessControl = { Create: { name: "Create Post", description: "Permission to create new post records", }, Update: { name: "Update Post", description: "Permission to update existing post records", }, Delete: { name: "Delete Post", description: "Permission to delete post records", }, View: { name: "View Post", description: "Permission to view post records", }, } as const satisfies AuthConfigs["accessControl"]; function createPostPermission(action: string) { return authService.permission(action, "post", postAccessControl); } export const postPermissions = { canCreate: createPostPermission("Create"), canUpdate: createPostPermission("Update"), canDelete: createPostPermission("Delete"), canView: createPostPermission("View"), }; export const postAuthenticationControl = { Create: true, Update: true, Delete: true, View: true, }; const postAuthConfigs: AuthConfigs = { authenticationControl: postAuthenticationControl, accessControl: postAccessControl, }; export default postAuthConfigs; ``` Pass `--advanced` to generate a dynamic permissions object using `Object.keys` instead of the explicit helper function: ```bash arkos g a -m post --advanced ``` ## Auth Validation [#auth-validation] For generating login, signup, update-me, and update-password schemas and DTOs, see the [Validation](/docs/tooling/cli/code-generation/validation) guide — those commands are scoped to the `auth` module specifically. ## Related Guides [#related-guides] * [Code Generation Overview](/docs/tooling/cli/code-generation/overview) * [Core Components](/docs/tooling/cli/code-generation/core) * [Validation](/docs/tooling/cli/code-generation/validation) * [Route Hook](/docs/core-concepts/components/route-hooks) # Core Components These commands generate the structural files that make up an Arkos.js module — the controller, service, router, interceptors, and hooks. All commands follow the same convention: pass the module name with `-m` and the CLI writes the file to `src/modules//`. ## Controller [#controller] ```bash arkos generate controller --module post arkos g c -m post ``` **Output:** `src/modules/post/post.controller.ts` ```ts import { BaseController } from "arkos/controllers"; export class PostController extends BaseController {} const postController = new PostController("post"); export default postController; ``` For standard Prisma model modules, the controller extends `BaseController`, which already provides built-in implementations for `createOne`, `createMany`, `findOne`, `findMany`, `updateOne`, `updateMany`, `deleteOne`, and `deleteMany`. Add custom methods directly to the class. For non-Prisma modules (those not present in your Prisma schema), the generator produces a plain class without extending `BaseController`: ```ts export class ReportController {} const reportController = new ReportController(); export default reportController; ``` ## Service [#service] ```bash arkos generate service --module post arkos g s -m post ``` **Output:** `src/modules/post/post.service.ts` ```ts import { BaseService } from "arkos/services"; export class PostService extends BaseService<"post"> {} const postService = new PostService("post"); export default postService; ``` The `--module` flag also accepts the special values `auth` and `file-upload` to generate services for those built-in modules. Not all component types are available for those modules — the CLI will show an error if you request an unsupported combination. ## Router [#router] ```bash arkos generate router --module post arkos g r -m post ``` **Output:** `src/modules/post/post.router.ts` ```ts import { ArkosRouter } from "arkos"; import { RouteHook } from "arkos"; export const hook: RouteHook<"prisma"> = {} const postRouter = ArkosRouter({ prefix: "posts", openapi: { tags: ["Post"] } }) export default postRouter ``` For `auth` and `file-upload` modules the `RouteHook` type parameter is set accordingly (`"auth"` or `"file-upload"`). The `prefix` for `auth` is `"auth"` and for file upload it is read from your config. The generated `hook` export is the `RouteHook` — the named export that configures Arkos's built-in routes for this module. You populate its keys to add validation, authentication, rate limiting, and other per-route config. See the [Route Hook](/docs/core-concepts/components/route-hooks) guide for all available keys. `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](/docs/core-concepts/components/route-hooks) for full details. ## Interceptors [#interceptors] ```bash arkos generate interceptors --module post arkos g i -m post ``` **Output:** `src/modules/post/post.interceptors.ts` Interceptors let you hook into the request lifecycle of built-in routes without replacing Arkos's core logic. The generated file exports arrays for every lifecycle position across all operations: ```ts export const beforeCreateOne = [] export const afterCreateOne = [] export const onCreateOneError = [] export const beforeFindOne = [] export const afterFindOne = [] export const onFindOneError = [] export const beforeFindMany = [] export const afterFindMany = [] export const onFindManyError = [] export const beforeUpdateOne = [] export const afterUpdateOne = [] export const onUpdateOneError = [] export const beforeDeleteOne = [] export const afterDeleteOne = [] export const onDeleteOneError = [] export const beforeCreateMany = [] export const afterCreateMany = [] export const onCreateManyError = [] export const beforeUpdateMany = [] export const afterUpdateMany = [] export const onUpdateManyError = [] export const beforeDeleteMany = [] export const afterDeleteMany = [] export const onDeleteManyError = [] ``` For `auth` modules the exported names match auth operations (`beforeLogin`, `afterSignup`, `onUpdatePasswordError`, etc.). For `file-upload` modules they match file operations (`beforeUploadFile`, `afterDeleteFile`, etc.). Interceptors are only available for known modules — Prisma models, `auth`, and `file-upload`. Running the command for an unknown module will produce an error. ## Service Hooks [#service-hooks] ```bash arkos generate hooks --module post arkos g h -m post ``` **Output:** `src/modules/post/post.hooks.ts` Service hooks let you tap into the `BaseService` lifecycle at the service layer, before or after database operations. The generated file imports your service and exports arrays for every operation: ```ts import postService from "./post.service"; export const beforeFindOne = []; export const afterFindOne = []; export const onFindOneError = []; export const beforeUpdateOne = []; export const afterUpdateOne = []; export const onUpdateOneError = []; export const beforeCreateOne = []; export const afterCreateOne = []; export const onCreateOneError = []; export const beforeCreateMany = []; export const afterCreateMany = []; export const onCreateManyError = []; export const beforeCount = []; export const afterCount = []; export const onCountError = []; export const beforeFindMany = []; export const afterFindMany = []; export const onFindManyError = []; export const beforeUpdateMany = []; export const afterUpdateMany = []; export const onUpdateManyError = []; export const beforeDeleteOne = []; export const afterDeleteOne = []; export const onDeleteOneError = []; export const beforeDeleteMany = []; export const afterDeleteMany = []; export const onDeleteManyError = []; ``` Like interceptors, hooks are only available for known modules. ## Custom Paths [#custom-paths] All commands accept `-p` to write to a non-default location: ```bash arkos g c -m post -p src/api/modules # → src/api/modules/post/post.controller.ts ``` ## Related Guides [#related-guides] * [Code Generation Overview](/docs/tooling/cli/code-generation/overview) * [Prisma Components](/docs/tooling/cli/code-generation/prisma) * [Validation](/docs/tooling/cli/code-generation/validation) * [Authentication](/docs/tooling/cli/code-generation/authentication) # Overview The `arkos generate` command (alias `arkos g`) creates boilerplate files that follow Arkos.js conventions. Instead of writing the same scaffolding by hand for every module, you describe what you want and the CLI generates it — correctly named, correctly typed, and ready to customize. ## Generating Multiple Components at Once [#generating-multiple-components-at-once] The most powerful entry point is `generate components`. It lets you generate any combination of components for one or more modules in a single command. ```bash arkos generate components [options] arkos g co [options] ``` **Options specific to this command:** | Flag | Alias | Description | | ------------------- | ------ | ------------------------------------------------ | | `--all` | | Generate all available components for the module | | `--names ` | `-n` | Comma-separated list of components to generate | | `--modules ` | `--ms` | Comma-separated list of module names | ### Generate Everything for a Module [#generate-everything-for-a-module] ```bash arkos generate components --module post --all arkos g co -m post --all ``` This generates the full set of files for the `post` module: ``` src/modules/post/ ├── post.controller.ts ├── post.service.ts ├── post.router.ts ├── post.interceptors.ts ├── post.hooks.ts ├── post.policy.ts ├── post.query.ts ├── schemas/ │ ├── post.schema.ts │ ├── create-post.schema.ts │ ├── update-post.schema.ts │ └── query-post.schema.ts └── dtos/ ├── post.dto.ts ├── create-post.dto.ts ├── update-post.dto.ts └── query-post.dto.ts prisma/schema/post.prisma ``` ### Generate Specific Components [#generate-specific-components] Pass a comma-separated list of component names or their aliases with `-n`: ```bash arkos generate components --module post --names service,controller,router arkos g co -m post -n s,c,r ``` You can mix full names and aliases freely: ```bash arkos g co -m post -n service,c,router,sc,dto ``` ### Generate Components for Multiple Modules at Once [#generate-components-for-multiple-modules-at-once] Use `--modules` (or `--ms`) to run the same generation across several modules: ```bash arkos generate components --modules post,user,comment --all arkos g co --ms post,user,comment --all # Or specific components across multiple modules arkos g co --ms post,user -n s,c,r ``` When using multiple modules, use `--modules` / `--ms`. Passing a comma-separated list to `-m` / `--module` is not supported and will throw an error. ## Available Components [#available-components] | Full Name | Alias | What It Generates | Guide | | --------------- | ----- | ------------------------------------------- | ------------------------------------------------------------------ | | `controller` | `c` | Controller class extending `BaseController` | [Core](/docs/tooling/cli/code-generation/core) | | `service` | `s` | Service class extending `BaseService` | [Core](/docs/tooling/cli/code-generation/core) | | `router` | `r` | Router file with `RouteHook` export | [Core](/docs/tooling/cli/code-generation/core) | | `interceptors` | `i` | Interceptor middleware arrays | [Core](/docs/tooling/cli/code-generation/core) | | `hooks` | `h` | Service hook arrays | [Core](/docs/tooling/cli/code-generation/core) | | `query-options` | `q` | Prisma query options config | [Prisma](/docs/tooling/cli/code-generation/prisma) | | `model` | `m` | Prisma model file | [Prisma](/docs/tooling/cli/code-generation/prisma) | | `schema` | `sc` | Base Zod schema | [Validation](/docs/tooling/cli/code-generation/validation) | | `create-schema` | `cs` | Create Zod schema | [Validation](/docs/tooling/cli/code-generation/validation) | | `update-schema` | `us` | Update Zod schema | [Validation](/docs/tooling/cli/code-generation/validation) | | `query-schema` | `qs` | Query Zod schema | [Validation](/docs/tooling/cli/code-generation/validation) | | `dto` | `d` | Base class-validator DTO | [Validation](/docs/tooling/cli/code-generation/validation) | | `create-dto` | `cd` | Create DTO | [Validation](/docs/tooling/cli/code-generation/validation) | | `update-dto` | `ud` | Update DTO | [Validation](/docs/tooling/cli/code-generation/validation) | | `query-dto` | `qd` | Query DTO | [Validation](/docs/tooling/cli/code-generation/validation) | | `policy` | `p` | `ArkosPolicy` file | [Authentication](/docs/tooling/cli/code-generation/authentication) | | `auth-configs` | `a` | Auth config file (deprecated) | [Authentication](/docs/tooling/cli/code-generation/authentication) | ## Overwriting Existing Files [#overwriting-existing-files] By default the CLI refuses to overwrite an existing file. Pass `-o` or `--overwrite` to force it: ```bash arkos g co -m post --all --overwrite ``` ## Custom Output Path [#custom-output-path] Override the default `src/modules` location with `-p`: ```bash arkos g co -m post --all -p src/api # Results in: src/api/post/post.controller.ts, etc. ``` ## Related Guides [#related-guides] * [Core Components](/docs/tooling/cli/code-generation/core) * [Prisma Components](/docs/tooling/cli/code-generation/prisma) * [Validation](/docs/tooling/cli/code-generation/validation) * [Authentication](/docs/tooling/cli/code-generation/authentication) # Prisma These commands generate files that are tightly tied to your Prisma schema — the model file itself, and the query options that control how Arkos builds Prisma queries for each operation. ## Prisma Model [#prisma-model] ```bash arkos generate model --module product arkos g m -m product ``` **Output:** `prisma/schema/product.prisma` The generator reads your existing Prisma models to find fields that are common across all of them (typically `id`, `createdAt`, `updatedAt`, and any other timestamp fields you use consistently). It uses those as the starting point for the new model: ```prisma model Product { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } ``` If all your existing models use `@@map`, the new model will include a `@@map` directive as well. The output is a starting point — add your own fields after generating. Use `-p` to write to a custom path: ```bash arkos g m -m product -p prisma/modules # → prisma/modules/product.prisma ``` ## Query Options [#query-options] ```bash arkos generate query-options --module post arkos g q -m post ``` **Output:** `src/modules/post/post.query.ts` Query options let you customize the Prisma queries Arkos builds for each operation — adding `include`, `select`, `where`, `orderBy`, and any other Prisma options on a per-operation or global basis. ```ts import { Prisma } from "@prisma/client"; import { PrismaQueryOptions } from "arkos/prisma"; const postQueryOptions: PrismaQueryOptions = { global: {}, find: {}, findOne: {}, findMany: {}, update: {}, updateMany: {}, updateOne: {}, create: {}, createMany: {}, createOne: {}, save: {}, saveMany: {}, saveOne: {}, delete: {}, deleteMany: {}, deleteOne: {}, } export default postQueryOptions; ``` For the `auth` module the generated keys match auth operations instead: ```bash arkos g q -m auth ``` ```ts import { Prisma } from "@prisma/client"; import { PrismaQueryOptions } from "arkos/prisma"; const authQueryOptions: PrismaQueryOptions = { getMe: {}, updateMe: {}, deleteMe: {}, login: {}, signup: {}, updatePassword: {}, } export default authQueryOptions; ``` Query options are only available for known modules — Prisma models, `auth`, and `file-upload`. ## Related Guides [#related-guides] * [Code Generation Overview](/docs/tooling/cli/code-generation/overview) * [Core Components](/docs/tooling/cli/code-generation/core) * [Validation](/docs/tooling/cli/code-generation/validation) * [Authentication](/docs/tooling/cli/code-generation/authentication) # Validation Arkos supports two validation libraries — Zod and class-validator. Both have equivalent generation commands, and both generate files from your Prisma schema automatically. The generated schemas and DTOs are ready to pass directly into a `RouteHook` or `ArkosRouter` route config. Schemas are generated to `src/modules//schemas/` and DTOs to `src/modules//dtos/`. ## Model Validation [#model-validation] These commands generate validation for standard Prisma model routes. They read your Prisma schema and produce typed schemas or DTOs that match your model's fields, automatically handling optional fields, relations, enums, and composite types. ### Create [#create] ```bash arkos generate create-schema --module post arkos g cs -m post ``` **Output:** `src/modules/post/schemas/create-post.schema.ts` Fields marked as `@id`, timestamps (`createdAt`, `updatedAt`, `deletedAt`), and foreign key columns are excluded. Relation fields are converted to nested objects containing just the reference field (e.g. `{ id: z.string() }`). Optional fields and fields with defaults get `.optional()`. Enum fields use `z.nativeEnum()`. ```ts import { z } from "zod"; const CreatePostSchema = z.object({ title: z.string(), content: z.string().optional(), published: z.boolean().optional(), author: z.object({ id: z.string().min(1) }), }); export default CreatePostSchema; export type CreatePostSchemaType = z.infer; ``` ```bash arkos generate create-dto --module post arkos g cd -m post ``` **Output:** `src/modules/post/dtos/create-post.dto.ts` The same field exclusion and relation logic applies. Each field gets the appropriate class-validator decorators. Only the decorators actually needed for the model are imported. ```ts import { IsNotEmpty, IsOptional, IsString, IsBoolean, ValidateNested } from "class-validator"; import { Type } from "class-transformer"; class AuthorForCreatePostDto { @IsNotEmpty() @IsString() id!: string; } export default class CreatePostDto { @IsNotEmpty() @IsString() title!: string; @IsOptional() @IsNotEmpty() @IsString() content?: string; @IsOptional() @IsBoolean() published?: boolean; @IsOptional() @ValidateNested() @Type(() => AuthorForCreatePostDto) author?: AuthorForCreatePostDto; } ``` ### Update [#update] ```bash arkos generate update-schema --module post arkos g us -m post ``` **Output:** `src/modules/post/schemas/update-post.schema.ts` All fields are made optional for patch semantics. The same relation and enum handling applies. ```ts import { z } from "zod"; const UpdatePostSchema = z.object({ title: z.string().optional(), content: z.string().optional(), published: z.boolean().optional(), author: z.object({ id: z.string().min(1) }).optional(), }); export default UpdatePostSchema; export type UpdatePostSchemaType = z.infer; ``` ```bash arkos generate update-dto --module post arkos g ud -m post ``` **Output:** `src/modules/post/dtos/update-post.dto.ts` Every field gets `@IsOptional()`. All other decorators still apply. ```ts import { IsOptional, IsNotEmpty, IsString, IsBoolean, ValidateNested } from "class-validator"; import { Type } from "class-transformer"; class AuthorForUpdatePostDto { @IsNotEmpty() @IsString() id!: string; } export default class UpdatePostDto { @IsOptional() @IsNotEmpty() @IsString() title?: string; @IsOptional() @IsNotEmpty() @IsString() content?: string; @IsOptional() @IsBoolean() published?: boolean; @IsOptional() @ValidateNested() @Type(() => AuthorForUpdatePostDto) author?: AuthorForUpdatePostDto; } ``` ### Query [#query] ```bash arkos generate query-schema --module post arkos g qs -m post ``` **Output:** `src/modules/post/schemas/query-post.schema.ts` Includes `page`, `limit`, `sort`, and `fields` pagination fields, then adds filter schemas per field type — string fields get `icontains`, number fields get `equals/gte/lte`, datetime fields get `equals/gte/lte`. ```ts import { z } from "zod"; const StringFilterSchema = z.object({ icontains: z.string().optional() }); const QueryPostSchema = z.object({ page: z.coerce.number().optional(), limit: z.coerce.number().max(100).optional(), sort: z.string().optional(), fields: z.string().optional(), title: StringFilterSchema.optional(), content: StringFilterSchema.optional(), published: z.boolean().optional(), createdAt: StringFilterSchema.optional(), updatedAt: StringFilterSchema.optional(), }); export default QueryPostSchema; export type QueryPostSchemaType = z.infer; ``` ```bash arkos generate query-dto --module post arkos g qd -m post ``` **Output:** `src/modules/post/dtos/query-post.dto.ts` Same structure, using class-validator filter classes instead of Zod filter schemas. ```ts import { IsOptional, IsString, IsNumber, IsBoolean, ValidateNested, Max, IsNotEmpty } from "class-validator"; import { Type, Transform } from "class-transformer"; class StringFilter { @IsOptional() @IsString() @Type(() => String) icontains?: string; } export default class PostQueryDto { @IsOptional() @IsNumber() @Transform(({ value }) => (value ? Number(value) : undefined)) page?: number; @IsOptional() @IsNumber() @Max(100) @Transform(({ value }) => (value ? Number(value) : undefined)) limit?: number; @IsOptional() @IsNotEmpty() @IsString() @Type(() => String) sort?: string; @IsOptional() @IsNotEmpty() @IsString() @Type(() => String) fields?: string; @IsOptional() @ValidateNested() @Type(() => StringFilter) title?: StringFilter; @IsOptional() @IsBoolean() published?: boolean; } ``` ### Base [#base] The base schema/DTO includes all fields from the model without create or update semantics — useful as a reference type or for response shapes. ```bash arkos generate schema --module post arkos g sc -m post ``` **Output:** `src/modules/post/schemas/post.schema.ts` Relation fields are excluded. All other fields are included as-is with their optionality from the Prisma schema. ```bash arkos generate dto --module post arkos g d -m post ``` **Output:** `src/modules/post/dtos/post.dto.ts` ## Auth Validation [#auth-validation] These commands generate validation specifically for the authentication routes (`login`, `signup`, `updateMe`, `updatePassword`). They are always scoped to the `auth` module and always write to `src/modules/auth/schemas/` or `src/modules/auth/dtos/`. All commands require `-m auth`: ```bash arkos g ls -m auth # login-schema arkos g ss -m auth # signup-schema arkos g ums -m auth # update-me-schema arkos g ups -m auth # update-password-schema ``` ### Login [#login] ```bash arkos generate login-schema -m auth arkos g ls -m auth ``` **Output:** `src/modules/auth/schemas/login.schema.ts` The generated username fields are driven by your `authentication.login.allowedUsernames` config. If you allow multiple username fields (e.g. `email` and `username`) each is generated as optional, with a comment indicating at least one is required. Defaults to `username` if not configured. ```ts import { z } from "zod"; // At least one of: email, username is required const LoginSchema = z.object({ email: z.string().email().optional(), username: z.string().min(1).optional(), password: z.string().min(8) }); export default LoginSchema; export type LoginSchemaType = z.infer; ``` ```bash arkos generate login-dto -m auth arkos g ld -m auth ``` **Output:** `src/modules/auth/dtos/login.dto.ts` ```ts import { IsOptional, IsEmail, IsString, IsNotEmpty, MinLength, Matches } from "class-validator"; export default class LoginDto { @IsOptional() @IsEmail() email?: string; @IsOptional() @IsString() username?: string; @IsNotEmpty() @IsString() @MinLength(8) @Matches(/[a-z]/, { message: "Must contain lowercase" }) @Matches(/[A-Z]/, { message: "Must contain uppercase" }) @Matches(/[0-9]/, { message: "Must contain number" }) password!: string; } ``` ### Signup [#signup] ```bash arkos generate signup-schema -m auth arkos g ss -m auth ``` **Output:** `src/modules/auth/schemas/signup.schema.ts` Generated from your `User` Prisma model. Restricted fields (`roles`, `isActive`, `isStaff`, `isSuperUser`, `passwordChangedAt`, and similar internal fields) are excluded. The `password` field always gets strong regex validation. ```ts import { z } from "zod"; const SignupSchema = z.object({ username: z.string(), email: z.string().email(), password: z.string().min(8) .regex(/[a-z]/, "Must contain lowercase") .regex(/[A-Z]/, "Must contain uppercase") .regex(/[0-9]/, "Must contain number"), }); export default SignupSchema; export type SignupSchemaType = z.infer; ``` ```bash arkos generate signup-dto -m auth arkos g sd -m auth ``` **Output:** `src/modules/auth/dtos/signup.dto.ts` ```ts import { IsNotEmpty, IsString, IsEmail, MinLength, Matches } from "class-validator"; export default class SignupDto { @IsNotEmpty() @IsString() username!: string; @IsNotEmpty() @IsEmail() email!: string; @IsNotEmpty() @IsString() @MinLength(8) @Matches(/[a-z]/, { message: "Must contain lowercase" }) @Matches(/[A-Z]/, { message: "Must contain uppercase" }) @Matches(/[0-9]/, { message: "Must contain number" }) password!: string; } ``` ### Update Me [#update-me] ```bash arkos generate update-me-schema -m auth arkos g ums -m auth ``` **Output:** `src/modules/auth/schemas/update-me.schema.ts` Like signup but all fields are optional, and `password` is excluded (password changes go through the dedicated `updatePassword` route). ```ts import { z } from "zod"; const UpdateMeSchema = z.object({ username: z.string().optional(), email: z.string().email().optional(), }); export default UpdateMeSchema; export type UpdateMeSchemaType = z.infer; ``` ```bash arkos generate update-me-dto -m auth arkos g umd -m auth ``` **Output:** `src/modules/auth/dtos/update-me.dto.ts` ```ts import { IsOptional, IsNotEmpty, IsString, IsEmail } from "class-validator"; export default class UpdateMeDto { @IsOptional() @IsNotEmpty() @IsString() username?: string; @IsOptional() @IsEmail() email?: string; } ``` ### Update Password [#update-password] ```bash arkos generate update-password-schema -m auth arkos g ups -m auth ``` **Output:** `src/modules/auth/schemas/update-password.schema.ts` ```ts import { z } from "zod"; const UpdatePasswordSchema = z.object({ currentPassword: z.string().min(1), newPassword: z.string().min(8) .regex(/[a-z]/, "Must contain lowercase") .regex(/[A-Z]/, "Must contain uppercase") .regex(/[0-9]/, "Must contain number") }); export default UpdatePasswordSchema; export type UpdatePasswordSchemaType = z.infer; ``` ```bash arkos generate update-password-dto -m auth arkos g upd -m auth ``` **Output:** `src/modules/auth/dtos/update-password.dto.ts` ```ts import { IsNotEmpty, IsString, MinLength, Matches } from "class-validator"; export default class UpdatePasswordDto { @IsNotEmpty() @IsString() currentPassword!: string; @IsNotEmpty() @IsString() @MinLength(8) @Matches(/[a-z]/, { message: "Must contain lowercase" }) @Matches(/[A-Z]/, { message: "Must contain uppercase" }) @Matches(/[0-9]/, { message: "Must contain number" }) newPassword!: string; } ``` ## Notes [#notes] Model validation commands (`create-schema`, `update-schema`, etc.) are only available for modules that exist in your Prisma schema. Running them for an unknown module produces an error. Auth validation commands are only available with `-m auth`. All generated files use the field types, optionality, and enum values from your actual Prisma schema — regenerate after schema changes. ## Related Guides [#related-guides] * [Code Generation Overview](/docs/tooling/cli/code-generation/overview) * [Core Components](/docs/tooling/cli/code-generation/core) * [Authentication](/docs/tooling/cli/code-generation/authentication) # Development The Arkos.js CLI covers the full lifecycle of running your application — from hot-reload development to optimized production deployments. ## `arkos dev` [#arkos-dev] Starts a development server with hot-reload. The server restarts automatically whenever source files, configuration files, or `.env` files change. ```bash arkos dev [options] ``` | Option | Description | | --------------------- | ------------------ | | `-p, --port ` | Custom port number | | `-h, --host ` | Host to bind to | ```bash # Start on the default port arkos dev # Start on a custom port and host arkos dev --port 4000 --host 0.0.0.0 ``` The server prints startup info and logs each restart with a timestamp: ```bash Arkos.js 1.6.0 - Local: http://localhost:8000 - Environments: .env, .env.local 12:34:56 Restarting: src/modules/post/post.controller.ts changed ``` TypeScript projects are run with [`tsx-strict`](https://github.com/uanela/tsx-strict) for type-checked execution. JavaScript projects use the same runner without the type-check step. ## `arkos build` [#arkos-build] Compiles your application into an optimized production build. ```bash arkos build ``` TypeScript projects are compiled to JavaScript using a temporary tsconfig. JavaScript projects are copied and processed directly. The output is written to `.build/`. ```bash Arkos.js 1.6.0 - Environments: .env Creating an optimized production build... Build complete! Next step: Run it using npm run start ``` ## `arkos start` [#arkos-start] Runs the production build generated by `arkos build`. Always build first — `arkos start` looks for the compiled output at `.build/src/app.js`. ```bash arkos start [options] ``` | Option | Description | | --------------------- | ------------------ | | `-p, --port ` | Custom port number | | `-h, --host ` | Host to bind to | ```bash arkos start arkos start --port 3000 --host 0.0.0.0 ``` ## Recommended `package.json` Scripts [#recommended-packagejson-scripts] When you create a project with `create-arkos`, these scripts are added automatically. If you set up manually, add them yourself: ```json { "scripts": { "dev": "arkos dev", "build": "arkos build", "start": "arkos start", "arkos": "arkos" } } ``` ## CI/CD [#cicd] ```bash npm install arkos build arkos start --port $PORT ``` ## Related Guides [#related-guides] * [CLI Overview](/docs/tooling/cli/overview) * [Components Generation](/docs/tooling/cli/code-generation/overview) * [TypeScript](/docs/tooling/cli/typescript) # Overview The Arkos.js CLI is a built-in command-line tool available inside any Arkos.js project. It handles your development server, production builds, and all code generation — giving you a consistent, convention-driven workflow without repetitive boilerplate. Unlike `create-arkos`, which bootstraps new projects, this CLI lives inside your project and is used day-to-day. ```bash arkos [command] [options] ``` ## Command Reference [#command-reference] ### Server Commands [#server-commands] | Command | Description | | ------------- | ---------------------------------------- | | `arkos dev` | Start development server with hot-reload | | `arkos build` | Compile an optimized production build | | `arkos start` | Run the production build | ### Code Generation Commands [#code-generation-commands] | Command | Alias | Description | Since | | --------------------------------------- | ------------- | ------------------------------------ | ----- | | `arkos generate controller` | `arkos g c` | Controller class | v1.3 | | `arkos generate service` | `arkos g s` | Service class | v1.3 | | `arkos generate router` | `arkos g r` | Router with `RouteHook` | v1.3 | | `arkos generate interceptors` | `arkos g i` | Interceptors file | v1.3 | | `arkos generate hooks` | `arkos g h` | Service hooks file | v1.3 | | `arkos generate schema` | `arkos g sc` | Base Zod schema | v1.5 | | `arkos generate create-schema` | `arkos g cs` | Create Zod schema | v1.4 | | `arkos generate update-schema` | `arkos g us` | Update Zod schema | v1.4 | | `arkos generate query-schema` | `arkos g qs` | Query Zod schema | v1.5 | | `arkos generate dto` | `arkos g d` | Base class-validator DTO | v1.5 | | `arkos generate create-dto` | `arkos g cd` | Create DTO | v1.4 | | `arkos generate update-dto` | `arkos g ud` | Update DTO | v1.4 | | `arkos generate query-dto` | `arkos g qd` | Query DTO | v1.5 | | `arkos generate query-options` | `arkos g q` | Prisma query options | v1.3 | | `arkos generate model` | `arkos g m` | Prisma model file | v1.5 | | `arkos generate policy` | `arkos g p` | ArkosPolicy file | v1.6 | | `arkos generate auth-configs` | `arkos g a` | Auth config file (deprecated) | v1.3 | | `arkos generate login-schema` | `arkos g ls` | Login Zod schema | v1.6 | | `arkos generate signup-schema` | `arkos g ss` | Signup Zod schema | v1.6 | | `arkos generate update-me-schema` | `arkos g ums` | Update-me Zod schema | v1.6 | | `arkos generate update-password-schema` | `arkos g ups` | Update-password Zod schema | v1.6 | | `arkos generate login-dto` | `arkos g ld` | Login DTO | v1.6 | | `arkos generate signup-dto` | `arkos g sd` | Signup DTO | v1.6 | | `arkos generate update-me-dto` | `arkos g umd` | Update-me DTO | v1.6 | | `arkos generate update-password-dto` | `arkos g upd` | Update-password DTO | v1.6 | | `arkos generate components` | `arkos g co` | Generate multiple components at once | v1.5 | ### TypeScript Commands [#typescript-commands] | Command | Alias | Description | Since | | ----------------------- | ----------- | ------------------------------------------- | ----- | | `arkos prisma generate` | `arkos p g` | Generate Prisma client and sync Arkos types | v1.4 | ## Common Flags [#common-flags] All `generate` commands share these flags: | Flag | Description | | --------------------- | -------------------------------------------------------- | | `-m, --module ` | Module name (recommended) | | `--model ` | Module name — deprecated alias, use `--module` | | `--modules ` | Comma-separated module names for multi-module generation | | `-p, --path ` | Custom output path (default: `src/modules`) | | `-o, --overwrite` | Overwrite existing files | ## Project Structure [#project-structure] Generated files follow a consistent layout: ``` src/modules/ └── post/ ├── post.controller.ts ├── post.service.ts ├── post.router.ts ├── post.interceptors.ts ├── post.hooks.ts ├── post.policy.ts ├── post.query.ts ├── schemas/ │ ├── post.schema.ts │ ├── create-post.schema.ts │ ├── update-post.schema.ts │ └── query-post.schema.ts └── dtos/ ├── post.dto.ts ├── create-post.dto.ts ├── update-post.dto.ts └── query-post.dto.ts ``` ## Naming Conventions [#naming-conventions] | Style | Used for | Example | | ---------- | -------------------- | ---------------------------- | | kebab-case | Files, routes | `user-profile.controller.ts` | | camelCase | Variables, instances | `userProfileController` | | PascalCase | Classes, types | `UserProfileController` | The CLI detects whether your project uses TypeScript or JavaScript and generates files with the correct extension and type annotations automatically. ## Related Guides [#related-guides] * [Development](/docs/tooling/cli/development) * [Components Generation](/docs/tooling/cli/code-generation/overview) * [TypeScript](/docs/tooling/cli/typescript) # TypeScript ## `arkos prisma generate` [#arkos-prisma-generate] ```bash arkos prisma generate arkos p g ``` Runs `prisma generate` to produce the Prisma client, then syncs Arkos's internal type system with your current schema. This keeps `BaseService` type parameters, query option types, and other Arkos generics accurate after schema changes. Run this command whenever you: * Modify your Prisma schema * Pull schema changes from version control * See TypeScript errors related to Prisma model types in Arkos files The CLI detects your project type (TypeScript or JavaScript) and handles the generation accordingly. ## Related Guides [#related-guides] * [CLI Overview](/docs/tooling/cli/overview) * [Prisma Components](/docs/tooling/cli/code-generation/prisma) # Command Line Interface The built-in Arkos.js CLI provides powerful development commands for building, running, and generating components in your Arkos.js projects. Unlike the scaffolding CLI (`create-arkos`), this CLI is available within existing Arkos.js projects to streamline your development workflow. ## Available Commands [#available-commands] The Arkos.js CLI offers five main command categories: ```bash arkos [command] [options] ``` ### Development Commands [#development-commands] | Command | Description | Purpose | | ------------- | ---------------------- | ----------------------------------------- | | `arkos dev` | Run development server | Hot-reload development with file watching | | `arkos build` | Build for production | Create optimized production builds | | `arkos start` | Run production server | Start the built application | ### Code Generation Commands [#code-generation-commands] | Command | Alias | Description | Version | | ------------------------------ | ------------ | ------------------------------------ | ------- | | `arkos generate controller` | `arkos g c` | Generate a new controller | 1.3.0 | | `arkos generate service` | `arkos g s` | Generate a new service | 1.3.0 | | `arkos generate router` | `arkos g r` | Generate a new router | 1.3.0 | | `arkos generate auth-configs` | `arkos g a` | Generate auth configuration | 1.3.0 | | `arkos generate query-options` | `arkos g q` | Generate Prisma query options | 1.3.0 | | `arkos generate interceptors` | `arkos g i` | Generate interceptors file | 1.3.0 | | `arkos generate hooks` | `arkos g h` | Generate service hooks file | 1.3.0 | | `arkos generate schema` | `arkos g sc` | Generate base Zod schema | 1.5.0 | | `arkos generate create-schema` | `arkos g cs` | Generate create Zod schema | 1.4.0 | | `arkos generate update-schema` | `arkos g us` | Generate update Zod schema | 1.4.0 | | `arkos generate query-schema` | `arkos g qs` | Generate query Zod schema | 1.5.0 | | `arkos generate dto` | `arkos g d` | Generate base class-validator DTO | 1.5.0 | | `arkos generate create-dto` | `arkos g cd` | Generate create DTO | 1.4.0 | | `arkos generate update-dto` | `arkos g ud` | Generate update DTO | 1.4.0 | | `arkos generate query-dto` | `arkos g qd` | Generate query DTO | 1.5.0 | | `arkos generate model` | `arkos g m` | Generate Prisma model | 1.5.0 | | `arkos generate components` | `arkos g co` | Generate multiple components at once | 1.5.0 | :::info New in v1.4.0+ * `arkos generate interceptors` is now the recommended command (replaces `middlewares`) * `--module` flag is now preferred over `--model` for consistency ::: ### Utilities Exportation Commands [#utilities-exportation-commands] > Available from `v1.4.0-beta` | Command | Alias | Description | | -------------------------- | ------------ | ---------------------------------- | | `arkos export auth-action` | `arkos e ac` | Exports all auth-actions to a file | ### Typescript Types Generation [#typescript-types-generation] > Available from `v1.4.0-beta` | Command | Alias | Description | | ----------------------- | ----------- | ---------------------------------------------------------- | | `arkos prisma generate` | `arkos p g` | Generate Prisma client types and sync Arkos internal types | ## Development Server [#development-server] ### `arkos dev` [#arkos-dev] Starts a development server with hot-reload capabilities, automatically restarting when files change. ```bash arkos dev [options] ``` **Options:** * `-p, --port ` - Custom port number * `-h, --host ` - Host to bind to **Features:** * **File Watching**: Automatically detects changes in `src/`, configuration files, and environment files * **TypeScript Support**: Uses [`tsx-strict`](https://github.com/uanela/tsx-strict) for TypeScript projects * **JavaScript Support**: Uses [`tsx-strict`](https://github.com/uanela/tsx-strict) without type-check for JavaScript projects * **Environment Reload**: Restarts server when `.env` files change * **Smart Debouncing**: Prevents excessive restarts with intelligent delay **Example:** ```bash # Start dev server on default port arkos dev # Start on custom port and host arkos dev --port 4000 --host 0.0.0.0 ``` The development server provides real-time feedback: ``` Arkos.js 1.5.0 - Local: http://localhost:8000 - Environments: .env, .env.local 12:34:56 Restarting: src/controllers/user.controller.ts changed ``` ## Production Build [#production-build] ### `arkos build` [#arkos-build] Creates an optimized production build of your Arkos.js application. ```bash arkos build ``` **Features:** * **TypeScript Compilation**: Compiles TypeScript to JavaScript with custom tsconfig * **Environment Detection**: Automatically detects project type (TS/JS) **Build Process:** 2\. For TypeScript: Compiles with temporary tsconfig 3\. For JavaScript: Copies and processes JS files **Example:** ```bash arkos build ``` **Output:** ``` Arkos.js 1.5.0 - Environments: .env Creating an optimized production build... Build complete! Next step: Run it using npm run start ``` ## Production Server [#production-server] ### `arkos start` [#arkos-start] Runs the production build of your application. ```bash arkos start [options] ``` **Options:** * `-p, --port ` - Custom port number * `-h, --host ` - Host to bind to **Requirements:** * Must run `arkos build` first * Looks for built application at `.build/src/app.js` **Example:** ```bash # Start production server arkos start # Start with custom configuration arkos start --port 3000 --host 0.0.0.0 ``` ## Code Generation [#code-generation] The generate commands create boilerplate code following Arkos.js conventions and best practices. ### Common Options [#common-options] All generate commands support: * `-m, --module ` - **Required** - Module/component name (recommended v1.4.0+) * `--model ` - **Deprecated** - Module/component name (still works, use `--module` instead) * `-p, --path ` - Custom path (default: `src/modules`) * `-o, --overwrite` - Overwrite existing files :::info Module vs Model Starting with v1.4.0+, use `--module` instead of `--model` for better consistency. Both work, but `--module` is the recommended flag going forward. ::: ### Controller Generation [#controller-generation] ```bash arkos generate controller --module user arkos g c -m user ``` **Generated Template:** ```typescript import { BaseController } from "arkos/controllers"; class UserController extends BaseController{ } const userController = new UserController("user"); export default userController; ``` **Features:** * Extends `BaseController` for automatic customizable CRUD operations * Uses kebab-case for resource naming * Follows TypeScript/JavaScript project conventions ### Service Generation [#service-generation] ```bash arkos generate service --module user arkos g s -m user ``` **Special Module Values:** The `--module` (or `-m`) option can take special values: * **auth**: Generates component for the Authentication module * **file-upload**: Generates component for the File Upload module ```bash # Generate auth service arkos g s -m auth # Generate file-upload service arkos g s -m file-upload ``` :::warning Not all components are available for `auth` and `file-upload` modules. The CLI will show an error if you try to generate unsupported components for these modules. ::: **Generated Template:** ```typescript import { BaseService } from "arkos/services"; class UserService extends BaseService<"user"> { // Add your custom service methods here } const userService = new UserService("user"); export default userService; ``` **Features:** * Extends `BaseService` with Prisma type safety * Automatic Prisma client integration * Ready for custom business logic ### Router Generation [#router-generation] ```bash arkos generate router --module user arkos g r -m user ``` **Generated Template:** ```typescript import { ArkosRouter } from 'arkos' import { authService } from 'arkos/services' const userRouter = ArkosRouter() export default userRouter ``` **Features:** * Automatic pluralization for endpoint paths * Built-in authentication middleware integration * Controller auto-import (if file exists) * Access control setup ### Auth Configuration Generation [#auth-configuration-generation] ```bash arkos generate auth-configs --module post arkos g a -m post ``` Generates authentication configuration for role-based access control with separated authentication and authorization controls. ```ts import { AuthConfigs } from 'arkos/auth'; import { authService } from "arkos/services"; export const postAccessControl = { Create: { roles: ["Admin", "Editor"], name: "Create Post", description: "Permission to create new post records", }, Update: { roles: ["Admin", "Editor", "Author"], name: "Update Post", description: "Permission to update existing post records", }, Delete: { roles: ["Admin"], name: "Delete Post", description: "Permission to delete post records", }, View: { roles: ["*"], // Wildcard: all authenticated users name: "View Post", description: "Permission to view post records", }, } as const satisfies AuthConfigs["accessControl"]; function createPostPermission(action: string) { return authService.permission(action, "post", postAccessControl); } export const postPermissions = { canCreate: createPostPermission("Create"), canUpdate: createPostPermission("Update"), canDelete: createPostPermission("Delete"), canView: createPostPermission("View"), }; export const postAuthenticationControl = { Create: true, Update: true, Delete: true, View: true, }; const postAuthConfigs: AuthConfigs = { authenticationControl: postAuthenticationControl, accessControl: postAccessControl, }; export default postAuthConfigs; ``` **Features (v1.5.0+):** * Separated authentication control (who needs to be logged in) * Access control with permission helpers * Wildcard role support (`*` for all authenticated users) * Auto-generated permission helper functions (with `--advanced` flag) #### Advanced Auth Configs Generation [#advanced-auth-configs-generation] ```bash arkos generate auth-configs --module post --advanced arkos g a -m post -a ``` Everything remains the same the only change is that now you will have ```ts import { AuthConfigs } from 'arkos/auth'; import { authService } from "arkos/services"; export const postAccessControl = { Create: { roles: [], name: "Create Post", description: "Permission to create new post records", }, Update: { roles: [], name: "Update Post", description: "Permission to update existing post records", }, Delete: { roles: [], name: "Delete Post", description: "Permission to delete post records", }, View: { roles: [], name: "View Post", description: "Permission to view post records", }, } as const satisfies AuthConfigs["accessControl"]; type PostPermissionName = `can${keyof typeof postAccessControl & string}`; export const postPermissions = Object.keys(postAccessControl).reduce( (acc, key) => { acc[`can${key}` as PostPermissionName] = authService.permission( key, "post", postAccessControl ); return acc; }, {} as Record> ); export const postAuthenticationControl = { Create: true, Update: true, Delete: true, View: true, }; const postAuthConfigs: AuthConfigs = { authenticationControl: postAuthenticationControl, accessControl: postAccessControl, }; export default postAuthConfigs; ``` ### Query Options Generation [#query-options-generation] ```bash arkos generate query-options --module user arkos g q -m user ``` **Generated Template:** ```typescript import { Prisma } from "@prisma/client"; import { PrismaQueryOptions } from 'arkos/prisma'; const userQueryOptions: PrismaQueryOptions = { global: {}, find: {}, findOne: {}, findMany: {}, update: {}, updateMany: {}, updateOne: {}, create: {}, createMany: {}, createOne: {}, save: {}, saveMany: {}, saveOne: {}, delete: {}, deleteMany: {}, deleteOne: {}, } export default userQueryOptions; ``` **Features:** * Type-safe Prisma query configuration * Supports all CRUD operations * Special handling for auth models ### Interceptors Generation [#interceptors-generation] ```bash arkos generate interceptors --module user arkos g i -m user ``` Generates interceptor middleware files for request processing. **File Location:** `src/modules/user/user.interceptors.ts` :::info Replaces `generate middlewares` (v1.4.0+) The `interceptors` command is the new recommended way. The old `middlewares` command still works but shows deprecation warnings and will be removed in v1.6.0. ::: ### Service Hooks Generation [#service-hooks-generation] ```bash arkos generate hooks --module user arkos g h -m user ``` Generates service hook files for customizing BaseService behavior at the service layer. **File Location:** `src/modules/user/user.hooks.ts` ### Schema Generation (Zod) [#schema-generation-zod] Generate Zod validation schemas for your Prisma models: ```bash # Base schema (all fields) arkos generate schema --module user arkos g sc -m user # Create schema (fields needed for creation) arkos generate create-schema --module user arkos g cs -m user # Update schema (fields that can be updated) arkos generate update-schema --module user arkos g us -m user # Query schema (fields for filtering/searching) arkos generate query-schema --module user arkos g qs -m user ``` **Generated Location:** `src/modules/user/schemas/` **Features:** * Auto-generates from Prisma schema * Supports all Prisma field types * Nested object and relation support ### DTO Generation (class-validator) [#dto-generation-class-validator] Generate class-validator DTOs for your Prisma models: ```bash # Base DTO (all fields) arkos generate dto --module user arkos g d -m user # Create DTO arkos generate create-dto --module user arkos g cd -m user # Update DTO arkos generate update-dto --module user arkos g ud -m user # Query DTO arkos generate query-dto --module user arkos g qd -m user ``` **Generated Location:** `src/modules/user/dtos/` ### Prisma Model Generation [#prisma-model-generation] ```bash arkos generate model --module product arkos g m -m product ``` **Generated Location:** `prisma/schema/` (customizable with `--path`) **Features:** * Generates basic Prisma model template * Includes common fields (id, createdAt, updatedAt) from your existing models * Ready for customization ### Bulk Component Generation v1.5.0+ [#bulk-component-generation-v150] Generate multiple components for a module at once - the most powerful feature for rapid development: ```bash # Generate ALL components for a module arkos generate components --module post --all arkos g co -m post --all # Generate specific components (comma-separated) arkos generate components --module post --names service,controller,router,schema,dto arkos g co -m post -n s,c,r,sc,d # Mix full names and aliases arkos g co -m post -n service,c,router,sc,dto ``` **Available Component Names:** | Full Name | Alias | What It Generates | | --------------- | ----- | ------------------------- | | `service` | `s` | BaseService extension | | `controller` | `c` | BaseController extension | | `router` | `r` | ArkosRouter configuration | | `interceptors` | `i` | Interceptor middlewares | | `hooks` | `h` | Service hooks | | `auth-configs` | `a` | Auth configuration | | `query-options` | `q` | Prisma query options | | `schema` | `sc` | Base Zod schema | | `create-schema` | `cs` | Create Zod schema | | `update-schema` | `us` | Update Zod schema | | `query-schema` | `qs` | Query Zod schema | | `dto` | `d` | Base class-validator DTO | | `create-dto` | `cd` | Create DTO | | `update-dto` | `ud` | Update DTO | | `query-dto` | `qd` | Query DTO | | `model` | `m` | Prisma model | **Example - Generate Complete Module:** ```bash # Everything you need for a new module in one command arkos g co -m product --all ``` This generates: * `product.service.ts` * `product.controller.ts` * `product.router.ts` * `product.interceptors.ts` * `product.hooks.ts` * `product.auth.ts` * `product.query.ts` * `prisma/schema/product.prisma` * `schemas/product.schema.ts` * `schemas/create-product.schema.ts` * `schemas/update-product.schema.ts` * `schemas/query-product.schema.ts` **Time saved:** From 30+ minutes of manual setup to **5 seconds** ⚡ ## Utility Commands [#utility-commands] ### Export Auth Actions v1.4.0+ [#export-auth-actions-v140] Export all authentication actions to a TypeScript/JavaScript file for frontend integration: ```bash # Export to default location (src/modules/auth/utils/auth-actions.ts) arkos export auth-action arkos e ac # Overwrite existing file (instead of merging) arkos export auth-action --overwrite arkos e ac -o # Custom output path arkos export auth-action --path src/constants arkos e ac -p src/constants ``` **Generated File:** ```typescript // src/modules/auth/utils/auth-actions.ts const authActions = [ { resource: "post", action: "Create", roles: ["Editor", "Admin"], name: "Create Posts", description: "Allows creating new blog posts", }, // ... all your auth actions ]; export default authActions; ``` **Use Cases:** * Map permissions to UI elements * Hide/show features based on roles * Add i18n translations * Generate TypeScript types ### Prisma Generate v1.4.0+ [#prisma-generate-v140] Generate Prisma client types and sync Arkos internal types for better TypeScript support: ```bash arkos prisma generate arkos p g ``` **What It Does:** * Runs `prisma generate` to create Prisma client * Syncs Arkos's internal type system with your Prisma schema * Ensures type safety across BaseService and other Arkos features **When to Use:** * After modifying your Prisma schema * After pulling schema changes from version control * When TypeScript shows type errors related to Prisma models ## File Structure & Conventions [#file-structure--conventions] ### Generated Module Structure [#generated-module-structure] When generating components, Arkos.js creates organized module directories: ``` src/modules/ └── user/ ├── user.controller.ts ├── user.service.ts ├── user.router.ts ├── user.auth.ts ├── user.query.ts ├── user.interceptors.ts ├── user.hooks.ts ├── schemas/ │ ├── user.schema.ts │ ├── create-user.schema.ts │ ├── update-user.schema.ts │ └── query-user.schema.ts └── dtos/ ├── user.dto.ts ├── create-user.dto.ts ├── update-user.dto.ts └── query-user.dto.ts ``` ### Naming Conventions [#naming-conventions] Arkos.js uses consistent naming patterns: | Case Type | Usage | Example | | -------------- | ------------------------ | ---------------------------- | | **kebab-case** | Files, routes, resources | `user-profile.controller.ts` | | **camelCase** | Variables, instances | `userProfileController` | | **PascalCase** | Classes, types | `UserProfileController` | ### TypeScript vs JavaScript [#typescript-vs-javascript] The CLI automatically detects your project type: * **TypeScript Projects**: Uses `.ts` extension, includes type annotations * **JavaScript Projects**: Uses `.js` extension, omits TypeScript-specific features ## Advanced Usage [#advanced-usage] ### Custom Paths [#custom-paths] Override default module paths: ```bash # Generate in custom directory arkos g c -m user -p src/custom/modules # Results in: src/custom/modules/user/user.controller.ts ``` ### Overwriting Files [#overwriting-files] By default, Arkos prevents overwriting existing files. Use `-o` or `--overwrite` to force: ```bash # Overwrite existing service arkos g s -m user --overwrite # Bulk generation with overwrite arkos g co -m user --all --overwrite ``` ### Environment Integration [#environment-integration] The CLI automatically integrates with your environment setup: * Loads multiple `.env` files (`.env`, `.env.local`, etc.) * Watches environment files for changes in dev mode * Displays loaded environment files in startup info ## Error Handling & Debugging [#error-handling--debugging] ### Common Issues [#common-issues] **Build Errors:** * Ensure TypeScript compilation succeeds * Check for missing dependencies * Verify file permissions **Dev Server Issues:** * Port conflicts: Use `--port` option * File watching problems: Check file permissions * Memory issues: Restart the development server **Generation Errors:** * Invalid module names: Use alphanumeric characters * Path conflicts: Ensure target directories are writable * Missing dependencies: Run `npm install` ### Debug Output [#debug-output] The CLI provides detailed feedback: ```bash # Development server with file change notifications 12:34:56 [Info] Restarting: user.controller.ts changed # Build process with environment info Arkos.js 1.5.0 - Environments: .env, .env.local Creating an optimized production build... ``` ## Integration with Project Workflow [#integration-with-project-workflow] ### Development Workflow [#development-workflow] 1. **Start Development**: `arkos dev` 2. **Generate Components**: `arkos g co -m ModelName --all` 3. **Build for Production**: `arkos build` 4. **Deploy**: `arkos start` ### CI/CD Integration [#cicd-integration] ```bash # In your CI pipeline npm install arkos build arkos start --port $PORT ``` ### Package.json Scripts [#packagejson-scripts] Add these scripts to your `package.json` (automatically added when project is created using `create-arkos` CLI): ```json { "scripts": { "dev": "arkos dev", "build": "arkos build", "start": "arkos start", "arkos": "arkos" } } ``` ## Cross-Reference with Create-Arkos [#cross-reference-with-create-arkos] The built-in CLI complements the [scaffolding CLI](/docs/tooling/create-arkos): | Phase | Tool | Purpose | | ------------------- | ----------------------------- | ----------------------------------------- | | **Project Setup** | `create-arkos` | Bootstrap new projects with configuration | | **Development** | `arkos dev` | Hot-reload development server | | **Code Generation** | `arkos generate` | Create components and boilerplate | | **Production** | `arkos build` + `arkos start` | Deploy optimized applications | ## Summary [#summary] The built-in Arkos.js CLI transforms your development experience with: ### Key Benefits [#key-benefits] 1. **Integrated Development**: Seamless dev server with hot-reload 2. **Production Ready**: Optimized builds with module format support 3. **Code Generation**: Consistent, type-safe component scaffolding 4. **Best Practices**: Generated code follows Arkos.js conventions 5. **Developer Experience**: Smart file watching and environment integration ### Productivity Features [#productivity-features] * **Zero Configuration**: Works out-of-the-box with intelligent defaults * **TypeScript First**: Full TypeScript support with type safety * **Environment Aware**: Automatic environment file detection and reloading * **Cross-Platform**: Consistent behavior across operating systems * **Extensible**: Generated code serves as starting points for customization * **Bulk Generation (v1.5.0+)**: Create entire modules in seconds with `generate components` The built-in CLI handles the repetitive aspects of development, allowing you to focus on building your application's unique features and business logic. # Create Arkos CLI The official scaffolding tool for Arkos.js projects. It guides you through an interactive setup and generates a production-ready RESTful API with zero configuration — automatic CRUD, authentication, validation, and more, all built on Express.js and Prisma. ## Quick Start [#quick-start] Node.js 22.9 or higher is required before running the command. ```bash npm create arkos@latest my-arkos-project ``` ```bash yarn create arkos@latest my-arkos-project ``` ```bash pnpm create arkos@latest my-arkos-project ``` ## Interactive Setup [#interactive-setup] The CLI asks a series of questions to configure your project: ```bash ? Would you like to use TypeScript? Yes ? What db provider will be used for Prisma? postgresql ? Would you like to set up Validation? Yes ? Choose validation library: zod ? Would you like to set up Authentication? Yes ? Choose authentication type: dynamic ? Would you like to use authentication with Multiple Roles? Yes ? Choose default username field for login: email ? Would you like to use Strict Routing? No ``` ## Configuration Options [#configuration-options] ### TypeScript [#typescript] The CLI supports both TypeScript and JavaScript projects. If you choose JavaScript, the validation library is automatically set to Zod — class-validator requires TypeScript. ### Database Providers [#database-providers] | Provider | Use Case | | ----------- | --------------------------------------- | | PostgreSQL | Complex applications with relationships | | MongoDB | Flexible schema requirements | | MySQL | Traditional web applications | | SQLite | Development and small projects | | SQL Server | Enterprise environments | | CockroachDB | High-scale, distributed applications | ### Validation [#validation] * **Zod** — TypeScript-first schema validation * **class-validator** — Decorator-based validation (TypeScript only) ### Authentication [#authentication] * **Static** — File-based roles and permissions, no extra database tables, fast permission checks * **Dynamic** — Database-driven permissions via `auth-role` and `auth-permission` tables, scalable for complex applications * **Define Later** — Skip authentication setup and configure it when you're ready When using **dynamic** authentication, you will also be asked whether to support multiple roles per user. This option is not available for SQLite or static authentication. ### Username Field [#username-field] Choose how users identify themselves at login: * **Email** — recommended for most applications * **Username** — great for social platforms * **Define Later** — configure a custom field later ### Strict Routing [#strict-routing] Enables Express strict routing — `/posts` and `/posts/` are treated as different routes. Disabled by default. ## Generated Project Structure [#generated-project-structure] ``` my-arkos-project/ ├── prisma/ │ └── schema.prisma ├── src/ │ ├── utils/ │ │ └── prisma/ │ │ └── index.ts │ ├── app.ts │ └── arkos.config.ts ├── .env ├── .gitignore ├── package.json ├── tsconfig.json └── pnpm-lock.yaml ``` ## Getting Started [#getting-started] ### 1. Navigate to your project [#1-navigate-to-your-project] ```bash cd my-arkos-project ``` ### 2. Configure your database [#2-configure-your-database] Edit `.env` with your connection string: ```bash DATABASE_URL="postgresql://username:password@localhost:5432/mydb" ``` ```bash DATABASE_URL="mongodb://localhost:27017/mydb" ``` ```bash DATABASE_URL="mysql://username:password@localhost:3306/mydb" ``` ```bash DATABASE_URL="file:./dev.db" ``` ### 3. Set up Prisma [#3-set-up-prisma] ```bash npx prisma generate npx prisma db push ``` ### 4. Start development [#4-start-development] ```bash npm run dev ``` Your Arkos.js API is now running and ready to handle requests. ## Environment Variables [#environment-variables] ```bash # Database DATABASE_URL=your-database-connection-string # JWT (if authentication enabled) JWT_SECRET=your-jwt-secret JWT_EXPIRES_IN=90d # Server PORT=8000 ``` ## Beyond Scaffolding [#beyond-scaffolding] Once your project is created, use the built-in Arkos CLI to generate controllers, services, routers, schemas, and more on demand. See the [CLI guide](/docs/tooling/cli/overview).