Documentation
Build a validated Express REST API with Fireclass, Firebase Admin, typed queries, and structured errors.
This guide builds a complete REST API for a users collection with create,
list, read, update, and delete routes.
Example application
Browse the Fireclass Express example for the generated project structure and configuration.
src/
lib/
firebase.ts
fireclass.ts
models/
user.ts
server.ts
.env
fireclass.json
tsconfig.jsonStart from the Express setup, or initialize a prepared Node project:
npx fireclass init
npx fireclass doctorUse service-account environment values for real Firestore. Never commit this file.
PROJECT_ID=your-project-id
CLIENT_EMAIL=firebase-adminsdk-xxxxx@your-project-id.iam.gserviceaccount.com
PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
PORT=3000For the Firestore emulator, set FIRESTORE_EMULATOR_HOST and a project id; no
service-account credential is required.
import "dotenv/config";
import { cert, getApps, initializeApp } from "firebase-admin/app";
import { getFirestore, type Firestore } from "firebase-admin/firestore";
function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing environment variable: ${name}`);
return value;
}
export function getDb(): Firestore {
if (getApps().length === 0) {
const projectId = required("PROJECT_ID");
if (process.env.FIRESTORE_EMULATOR_HOST) {
initializeApp({ projectId });
} else {
initializeApp({
credential: cert({
projectId,
clientEmail: required("CLIENT_EMAIL"),
privateKey: required("PRIVATE_KEY").replace(/\\n/g, "\n"),
}),
});
}
}
return getFirestore();
}Create the binding once. Relative imports include .js in Node ESM TypeScript.
import "reflect-metadata";
import { createFireclass } from "@dharayush7/fireclass-js";
import { getDb } from "./firebase.js";
export const { BaseModel, adapter } = createFireclass(getDb());import { Collection } from "@dharayush7/fireclass-js";
import { Type } from "class-transformer";
import {
IsDate,
IsEmail,
IsInt,
IsOptional,
IsString,
Length,
Min,
} from "class-validator";
import { BaseModel } from "../lib/fireclass.js";
@Collection("users")
export class User extends BaseModel<User> {
@IsString()
@Length(2, 50)
name!: string;
@IsEmail()
email!: string;
@IsInt()
@Min(0)
age!: number;
@IsOptional()
@IsDate()
@Type(() => Date)
createdAt?: Date;
constructor(data?: Partial<User>) {
super(data);
Object.assign(this, data);
}
}The asyncHandler wrapper supports Express 4 and forwards rejected promises to
the Fireclass error middleware.
import "dotenv/config";
import express, {
type NextFunction,
type Request,
type Response,
} from "express";
import { fireclassErrorHandler } from "@dharayush7/fireclass-js";
import { User } from "./models/user.js";
const app = express();
app.use(express.json());
const asyncHandler =
(handler: (req: Request, res: Response) => Promise<unknown>) =>
(req: Request, res: Response, next: NextFunction) =>
handler(req, res).catch(next);
app.post(
"/users",
asyncHandler(async (req, res) => {
const user = new User({
...req.body,
createdAt: new Date(),
});
const id = await user.save();
res.status(201).json({ id, ...user });
}),
);
app.get(
"/users",
asyncHandler(async (req, res) => {
const minAge =
typeof req.query.minAge === "string"
? Number(req.query.minAge)
: undefined;
if (minAge !== undefined && !Number.isFinite(minAge)) {
res.status(400).json({ error: "InvalidMinimumAge" });
return;
}
const users = await User.findMany({
where: minAge !== undefined ? { age: { gte: minAge } } : undefined,
orderBy: minAge !== undefined ? { age: "asc" } : { name: "asc" },
limit: 50,
});
res.json(users);
}),
);
app.get(
"/users/:id",
asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
res.status(404).json({ error: "NotFound" });
return;
}
res.json(user);
}),
);
app.patch(
"/users/:id",
asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
res.status(404).json({ error: "NotFound" });
return;
}
Object.assign(user, req.body);
await user.save();
res.json(user);
}),
);
app.delete(
"/users/:id",
asyncHandler(async (req, res) => {
const removed = await User.deleteById(req.params.id);
if (!removed) {
res.status(404).json({ error: "NotFound" });
return;
}
res.json(removed);
}),
);
app.use(
fireclassErrorHandler({
validationStatus: 422,
fireclassStatus: 400,
}),
);
const port = Number(process.env.PORT ?? 3000);
app.listen(port, () => {
console.log(`API listening on http://localhost:${port}`);
});The range query orders by age because Firestore requires a range-filtered
field to be the first ordering field. Other combinations may require a
composite index.
Start the Firestore emulator in one terminal:
npx firebase-tools emulators:start --only firestoreStart the API in another:
FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 \
PROJECT_ID=fireclass-demo \
npm run dev# Create
curl -X POST http://localhost:3000/users \
-H 'content-type: application/json' \
-d '{"name":"Ada Lovelace","email":"ada@example.com","age":36}'
# List and filter
curl http://localhost:3000/users
curl 'http://localhost:3000/users?minAge=30'
# Read, update, and delete
curl http://localhost:3000/users/USER_ID
curl -X PATCH http://localhost:3000/users/USER_ID \
-H 'content-type: application/json' \
-d '{"age":37}'
curl -X DELETE http://localhost:3000/users/USER_IDAn invalid create or update returns the structured validation response described in the Node/Express API.
npx fireclass doctor and the project typecheck in CI.