GuidesFile Upload

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.

MethodEndpointDescriptionOperation
GET/api/uploads/:fileType/:fileNameServe/retrieve a filefindFile
POST/api/uploads/:fileTypeUpload filesuploadFile
PATCH/api/uploads/:fileType/:fileNameReplace an existing fileupdateFile
DELETE/api/uploads/:fileType/:fileNameDelete a filedeleteFile

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

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 for the full router upload guide.

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

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.

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:

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

Sending requests

Retrieve a file

GET /api/uploads/images/1234567890-photo.jpg

Response: The file is served directly as a static asset.

Upload a file

When uploading, the FormData field name must match the :fileType in the URL.

POST /api/uploads/images
Authorization: Bearer YOUR_API_TOKEN
Content-Type: multipart/form-data
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:

{
  "success": true,
  "message": "File uploaded successfully",
  "urls": ["http://localhost:8000/uploads/images/1234567890-photo.jpg"]
}

Image processing via query parameters:

POST /api/uploads/images?resizeTo=800&format=webp
ParameterDescription
?width=500Resize to width (may distort ratio)
?height=300Resize to height (may distort ratio)
?resizeTo=800Resize to fit within px, keeps ratio
?format=webpConvert to format

Replace a file

Automatically deletes the old file before uploading the new one.

PATCH /api/uploads/images/old-image.jpg
Content-Type: multipart/form-data
Authorization: Bearer YOUR_API_TOKEN
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:

{
  "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 /api/uploads/images/1234567890-photo.jpg
Authorization: Bearer YOUR_API_TOKEN

Response: 204 No Content

Common workflow

Upload a file first, then reference it in a subsequent model request:

// 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

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

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)$/,
      },
    },
  },
});
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;
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-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