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:
// 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:
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
| Handler | Usual route | What it does |
|---|---|---|
readAll | GET / | Lists documents with page, limit and sort, and returns pagination meta. See Read All |
read | GET /:id | Returns one document by id. See Read |
create | POST / | Validates the body, saves a new document and answers 201. See Create |
createBulk | POST /bulk | Validates and saves a JSON array of documents in one request. See Bulk Create |
update | PATCH /update/:id | Changes the fields that the body contains and keeps the others. See Update |
remove | DELETE /delete/:id | Deletes one document. See Delete |
removeMultiple | DELETE /delete-many | Deletes the documents whose ids are listed in the body |
removeAll | DELETE /delete-all | Deletes 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.
| Handler | Usual route | What it does |
|---|---|---|
addFile | POST /:id/addFile | Uploads files and adds them to a file field of the document |
updateFile | PATCH /:id/updateFile | Replaces one stored file. The body names it with fileId |
deleteFile | DELETE /:id/deleteFile | Removes 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.
| Handler | Generated route | What it does |
|---|---|---|
authMethods | GET /auth/methods | Lists the sign-in methods that the server offers, so a front end can show the right options |
signup | POST /auth/signup | Creates an account. With one-time codes, it emails a code first |
verifyOTP | POST /auth/verify-otp | Checks an emailed one-time code for sign-up, sign-in or a password reset |
login | POST /auth/login | Checks the email and password. It starts a session, or asks for a one-time code or a second factor first when those are on |
resendOTP | POST /auth/resend-otp | Sends a new one-time code within the cooldown and resend limits |
forgotPassword | POST /auth/forgot-password | Starts a password reset and answers 200 whether or not the email has an account |
resetPassword | POST /auth/reset-password | Sets a new password with the reset token |
logout | POST /auth/logout | Ends the session and clears both cookies |
refresh | POST /auth/refresh-token | Exchanges a valid refresh token for new tokens |
magicLinkRequest | POST /auth/magic-link | Emails a sign-in link |
magicLinkVerify | POST /auth/magic-link/verify | Redeems the token from the link and starts a session |
oauthProviders | GET /auth/oauth/providers | Lists the configured social sign-in providers |
oauthStart | GET /auth/oauth/:provider | Redirects the browser to the provider |
oauthCallback | GET /auth/oauth/:provider/callback | Receives the answer of the provider and signs the user in |
mfaStatus | GET /auth/mfa | Tells the signed-in user whether two-factor sign-in is on |
mfaSetup | POST /auth/mfa/totp/setup | Starts two-factor setup and returns the secret for an authenticator app |
mfaEnable | POST /auth/mfa/totp/enable | Confirms a code, turns two-factor on and returns the recovery codes once |
mfaVerify | POST /auth/mfa/verify | Finishes a sign-in with an authenticator code or a recovery code |
mfaDisable | POST /auth/mfa/disable | Turns two-factor off |
mfaRecoveryCodes | POST /auth/mfa/recovery-codes | Replaces 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.
| Handler | Generated route | What it does |
|---|---|---|
getMe | GET /account/me | Returns the profile of the signed-in user, without private fields |
updateMe | PATCH /account/me | Changes profile fields. Protected fields such as role are never accepted, and a new password needs currentPassword and ends every session |
deleteMe | DELETE /account/me | Deletes the account together with its sessions and sign-in records |
checkRole | GET /account/role | Answers { 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 to | Use |
|---|---|
| Run code or send an email before or after a built-in handler | An action on the route. See Actions |
| Reject a request, or change the data that a handler returns | The 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 own | A 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 body | A 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 route | What 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:
// 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();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.
:::