Express
Wire the repository into Express.js route handlers with schema-driven validation, and map the ORM’s
typed errors to HTTP responses with the bundled errorHandler middleware.
The ORM is framework-agnostic: a repository is just an object you construct once and share. For the error classes themselves, see Error Handling; for the schema strategy, see Schema Validation; for NestJS, see NestJS.
Basic setup
Section titled “Basic setup”Construct the repository once and export it. Your schema must not declare a top-level id — it
is rejected at construction, and the document name is the sole source of id (see
Document Identity). The returned repository is
fully typed and validated on every write.
import { FirestoreRepository } from 'flintfire';import { db } from '../config/firebase';import { userSchema, User } from '../schemas/user.schema';
export const userRepo = FirestoreRepository.withSchema(db, 'users', userSchema);Define your routes as thin handlers that call the repository and forward any thrown error to the
shared errorHandler middleware via next(error):
import express from 'express';import { userRepo } from '../repositories/user.repository';import { ValidationError, NotFoundError } from 'flintfire';
const router = express.Router();
router.post('/users', async (req, res, next) => { try { const user = await userRepo.create( { ...req.body, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, { returnDoc: true }, ); res.status(201).json(user); } catch (error) { next(error); // errorHandler middleware will process this }});
router.get('/users', async (req, res, next) => { try { const { page = 1, limit = 20, status } = req.query;
let query = userRepo.query();
if (status) { query = query.where('status', '==', status); }
const result = await query .orderBy('createdAt', 'desc') .offsetPaginate(Number(page), Number(limit));
res.json(result); } catch (error) { next(error); }});
router.get('/users/:id', async (req, res, next) => { try { const user = await userRepo.getById(userRepo.id(req.params.id));
if (!user) { throw new NotFoundError(`User with id ${req.params.id} not found`); }
res.json(user); } catch (error) { next(error); }});
router.patch('/users/:id', async (req, res, next) => { try { const user = await userRepo.update( userRepo.id(req.params.id), { ...req.body, updatedAt: new Date().toISOString(), }, { returnDoc: true }, ); res.json(user); } catch (error) { next(error); }});
router.delete('/users/:id', async (req, res, next) => { try { await userRepo.delete(userRepo.id(req.params.id)); res.status(204).send(); } catch (error) { next(error); }});
export default router;Register the routes and mount errorHandler last so it can translate ORM errors into HTTP
responses:
import express from 'express';import { errorHandler } from 'flintfire/express';import userRoutes from './routes/user.routes';
const app = express();
app.use(express.json());app.use('/api', userRoutes);app.use(errorHandler); // Must be last
app.listen(3000, () => { console.log('Server running on port 3000');});Notes on the API used above:
- Request-supplied ids are validated with
userRepo.id(req.params.id)before touching Firestore — a malformed id throwsInvalidDocumentIdError(→ 400) rather than escaping the collection. See Document Identity.- Reads use
getById(id), which returnsFirestoreDocument<User> | null— check fornull(or usegetByIdOrThrow(id)to get aNotFoundErrorinstead).- Offset pagination is
offsetPaginate(page, pageSize). Forward opaque cursor pagination ispaginate(pageSize, cursor?)and requires a priororderBy(). Typed bounds (startAt/startAfter/ …) and reverse pages (limitToLast) are also on the query builder — see Queries.update(id, data, { returnDoc: true })returns the updated document. A top-levelidis always stripped from write payloads, so...req.bodycannot repoint the document.
Error-handling middleware
Section titled “Error-handling middleware”The ORM includes a pre-built Express middleware for consistent error responses. It is published from
the optional flintfire/express subpath (not the package root), so
express stays out of the core type graph — install express to use it. Register it as the
last middleware, after all routes, and call next(error) from your route handlers (or throw and
let an async wrapper forward it).
It maps errors to HTTP status codes:
ValidationError→ 400 Bad RequestInvalidDocumentIdError→ 400 Bad Request (a malformed caller-supplied id; the body carries the machine-readablereason, never the raw id)InvalidPaginationCursorError→ 400 Bad Request (the body carries the machine-readablereason, never the cursor token or decoded document path)NotFoundError→ 404 Not FoundConflictError→ 409 Conflict (including create-only collisions fromcreateWithId)PreconditionFailedError→ 412 Precondition Failed (a failedlastUpdateTimewrite)FirestoreIndexError→ 503 Service Unavailable (a missing index is a server/config failure; the index-creation URL is deliberately not returned to the client — it stays server-side on the caught error’sindexUrlfor logging)- Others → 500 Internal Server Error
errorHandler(err: any, req: Request, res: Response, next: NextFunction): void
Maps errors to HTTP status codes and JSON bodies:
| Error | Status | Response body |
|---|---|---|
ValidationError |
400 | { error: 'ValidationError', details: issues } |
InvalidDocumentIdError |
400 | { error: 'InvalidDocumentIdError', reason } |
InvalidPaginationCursorError |
400 | { error: 'InvalidPaginationCursorError', reason } |
NotFoundError |
404 | { error: 'NotFoundError', message } |
FirestoreIndexError |
503 | { error: 'Query needs an index', message } |
ConflictError |
409 | { error: 'ConflictError', message } |
PreconditionFailedError |
412 | { error: 'PreconditionFailedError', message } |
WriteOutcomeError |
500 | { error: 'WriteOutcomeError', outcome } (cause is server-side only) |
| Anything else | 500 | { error: 'InternalServerError', message: 'Something went wrong' } |
The generic 500 branch intentionally hides the underlying message so internal details are not leaked
to clients. WriteOutcomeError exposes only the safe discriminated outcome — never cause,
stack, or cause message.
{ "error": "WriteOutcomeError", "outcome": { "state": "committed", "phase": "after-hook", "hook": { "event": "afterCreate", "execution": "direct", "retryable": false } }}See also
Section titled “See also”- Error Handling — the error classes and
parseFirestoreError - NestJS — the DI-based integration with an exception filter
- Schema Validation — the no-top-level-
idschema rule - Queries — pagination (
paginate,offsetPaginate) - CRUD operations —
create,update,delete, and bulk methods