S
Core Concepts

Handlers

The built-in handlers each collection type provides and how a route chooses one

A handler is the built-in code that a route runs. When you write handler: "readAll" on a route, you do not pass a function: you choose one of the operations that StarkForge already implements, and the route decides where it is served and who may call it. This page lists every handler by collection type, shows how a route picks one and explains what to do when none of them fits.

Choosing a Handler

A route names its handler with a string. The type of the collection decides which names are valid, and the method and path of the route decide where the handler is served:

TypeScript
// src/collections/products.ts (excerpt)
routes: defineRoutes([
  { method: "get", path: "/", handler: "readAll", auth: "public" },
  { method: "post", path: "/", handler: "create", auth: "authenticated", validation: "create" },
]),

Names are case-sensitive, and one handler may serve several routes. StarkForge checks every handler name when it builds the app, so a wrong name stops the server before it listens. The message ends with the list of names that the collection type offers:

Text
ConfigurationError (CONFIGURATION_ERROR): Invalid 'things' collection:
 - GET /things/: handler 'list' is not available for collections of type 'crud'. Available: read, readAll, create, createBulk, update, remove, removeMultiple, removeAll, addFile, updateFile, deleteFile.

In TypeScript, the handler property accepts the name of any built-in handler. Your editor catches a misspelled name, but not a valid name on the wrong type, such as login on a crud collection. The start-up check catches that one.

CRUD Handlers

A crud collection stores documents in the database. It has eight database handlers and three file handlers. The routes in the tables are the convention that this documentation uses. You are free to choose others, as Routes explains.

Database Handlers

HandlerUsual routeWhat it does
readAllGET /Lists documents with page, limit and sort, and returns pagination meta. See Read All
readGET /:idReturns one document by id. See Read
createPOST /Validates the body, saves a new document and answers 201. See Create
createBulkPOST /bulkValidates and saves a JSON array of documents in one request. See Bulk Create
updatePATCH /update/:idChanges the fields that the body contains and keeps the others. See Update
removeDELETE /delete/:idDeletes one document. See Delete
removeMultipleDELETE /delete-manyDeletes the documents whose ids are listed in the body
removeAllDELETE /delete-allDeletes every document that the caller may delete. It must use auth: "admin" or "admin-or-owner"

File Handlers

The three file handlers work on one document, so their paths start with its id. Each one needs a files entry on the route and the Cloudinary options, as Uploads shows.

HandlerUsual routeWhat it does
addFilePOST /:id/addFileUploads files and adds them to a file field of the document
updateFilePATCH /:id/updateFileReplaces one stored file. The body names it with fileId
deleteFileDELETE /:id/deleteFileRemoves one stored file, named by fileId in the body, from the document and from storage

Authentication Handlers

You rarely list these yourself. builtinConfig.auth generates an auth collection that holds the routes below, and it adds the routes for magic links, social sign-in and two-factor codes only when you configure those features. You list handlers yourself only when you write your own auth collection. The Authentication page covers both ways.

HandlerGenerated routeWhat it does
authMethodsGET /auth/methodsLists the sign-in methods that the server offers, so a front end can show the right options
signupPOST /auth/signupCreates an account. With one-time codes, it emails a code first
verifyOTPPOST /auth/verify-otpChecks an emailed one-time code for sign-up, sign-in or a password reset
loginPOST /auth/loginChecks the email and password. It starts a session, or asks for a one-time code or a second factor first when those are on
resendOTPPOST /auth/resend-otpSends a new one-time code within the cooldown and resend limits
forgotPasswordPOST /auth/forgot-passwordStarts a password reset and answers 200 whether or not the email has an account
resetPasswordPOST /auth/reset-passwordSets a new password with the reset token
logoutPOST /auth/logoutEnds the session and clears both cookies
refreshPOST /auth/refresh-tokenExchanges a valid refresh token for new tokens
magicLinkRequestPOST /auth/magic-linkEmails a sign-in link
magicLinkVerifyPOST /auth/magic-link/verifyRedeems the token from the link and starts a session
oauthProvidersGET /auth/oauth/providersLists the configured social sign-in providers
oauthStartGET /auth/oauth/:providerRedirects the browser to the provider
oauthCallbackGET /auth/oauth/:provider/callbackReceives the answer of the provider and signs the user in
mfaStatusGET /auth/mfaTells the signed-in user whether two-factor sign-in is on
mfaSetupPOST /auth/mfa/totp/setupStarts two-factor setup and returns the secret for an authenticator app
mfaEnablePOST /auth/mfa/totp/enableConfirms a code, turns two-factor on and returns the recovery codes once
mfaVerifyPOST /auth/mfa/verifyFinishes a sign-in with an authenticator code or a recovery code
mfaDisablePOST /auth/mfa/disableTurns two-factor off
mfaRecoveryCodesPOST /auth/mfa/recovery-codesReplaces the recovery codes

Account Handlers

The account collection works on the record of the signed-in user. builtinConfig.account generates it with the four handlers below, and every route requires sign-in.

HandlerGenerated routeWhat it does
getMeGET /account/meReturns the profile of the signed-in user, without private fields
updateMePATCH /account/meChanges profile fields. Protected fields such as role are never accepted, and a new password needs currentPassword and ends every session
deleteMeDELETE /account/meDeletes the account together with its sessions and sign-in records
checkRoleGET /account/roleAnswers { role, isAdmin } for any signed-in user

Health Handlers

A health collection has one handler for each HTTP method: healthGet, healthPost, healthPut, healthPatch and healthDelete. All five do the same work. They ping the database and answer 200 when it responds, or 503 when it does not. StarkForge adds GET /api/v1/health for you. Declare a health collection of your own to change its path or its access level.

The Deprecated Admin Handlers

The admin collection type has two handlers, checkRole and checkAdmin, that answer GET /admin/check. The type is kept for 1.x apps. checkAdmin is the old name of checkRole and logs a warning when you use it. New code should use checkRole in the account collection, as Collections explains.

When No Handler Fits

A route can name only the handlers in the tables above. There is no way to pass your own function as handler, because the handler is what validates the body, applies ownership, hides private fields and writes the response in the common format. When you need logic of your own, use the smallest tool that does the job:

You want toUse
Run code or send an email before or after a built-in handlerAn action on the route. See Actions
Reject a request, or change the data that a handler returnsThe same tool: a before action can throw to reject the request, and an after action can replace the response data
Add a new kind of collection with handlers of your ownA feature module that you register with forge.use(module) before build() or start()
Serve a plain Express route, such as a webhook that needs the raw bodyA route on forge.app, with the limits described below

A feature module is a class that extends FeatureModule, sets a type and returns a map of handler functions from handlers(). The crud, auth, account, health and admin types are feature modules too, and registering a module with one of those types replaces the built-in one. This is an advanced extension point. The TypeScript types of collection() list only the built-in types and handler names, so a collection for your own type needs a type assertion.

Your Own Express Routes

forge.app is the Express app that StarkForge serves, and you can add routes to it. StarkForge has no hook for adding routes or middleware inside its own pipeline, so where you add a route decides what happens to it:

When you add the routeWhat happens
Before build() or start()The route works, but it runs before every StarkForge stage. It gets no request id, secure headers, CORS check, rate limit, body parsing or error format
After build() or start()The route never runs. The not-found handler is already installed and answers 404 with the code ROUTE_NOT_FOUND

A route added before start() suits a simple endpoint that does not need those stages. It also suits a webhook that must read the raw body, because no StarkForge parser has touched the request yet, so Express's own express.raw() can read it. This server adds a ping route in the right place:

TypeScript
// src/server.ts
import { Forge } from "@starklabs/forge";

const forge = new Forge();

// This route runs before StarkForge's stages: no body parsing, CORS or rate limit.
forge.app.get("/ping", (_req, res) => {
  res.json({ pong: true });
});

await forge.start();
Routes After Start

Add your routes before you call start(). A route that you register after start() or build() is never reached, and nothing warns you: every request to it gets the not-found answer.

If an endpoint needs authentication, ownership, validation or the standard error format, build it as a collection, or as a feature module when no built-in handler does the work.

Where to Go Next

Actions

Run your own code or send an email around a handler.

Validation

Check every request body before a handler runs.

:::