GuidesFile Upload

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 and built-in routes via RouteHook.

If you need to upload files independently of any route or model operation, see File Upload Routes for the standalone approach instead.

Upload types

Arkos supports three upload strategies, all configured through the experimental.uploads key.

single

One file per request. The file path is attached to req.body[field] as a string.

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 for details.

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

Multiple files under one field. The paths are attached to req.body[field] as a string[].

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 for details.

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

Multiple named fields, each with its own maxCount. Use this when a single request carries files of fundamentally different kinds.

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;
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 for details.

Nested fields

Bracket notation maps uploaded files into nested req.body shapes. This works the same way in both ArkosRouter and RouteHook.

experimental: {
  uploads: {
    type: "single",
    field: "profile[photo]",
    uploadDir: "user-profiles",
  },
},

The file path lands at req.body.profile.photo.

experimental.uploads reference

PropertyTypeRequiredDefaultDescription
type"single" | "array" | "fields"Upload strategy
fieldstringFor single / arrayFormData field name. Supports bracket notation.
fieldsArray<{ name: string; maxCount: number }>For fieldsPer-field config
requiredbooleantrueMark file as optional with false
uploadDirstringAuto by MIMESubdirectory inside baseUploadDir
maxSizenumberFrom global configPer-file size limit in bytes
maxCountnumberFor arrayMax number of files
allowedFileTypesstring[] | RegExpFrom global configAllowed extensions or pattern
attachToBody"pathname" | "url" | "file" | false"pathname"How the file reference is attached to req.body
deleteOnErrorbooleantrueDelete uploaded files if the request fails