vv1.14.0
Main
This example demonstrates the complete data flow when creating a new domain, showing how all the architectural layers work together. We will analyze each file and explain how they interact.
We'll create a complete domain with:
bun gen:domain productnpm run gen:domain productyarn gen:domain productpnpm gen:domain productThis creates the complete structure. Let's examine each generated file.
src/domain/product/entity.ts) The entity defines the database table structure using Drizzle ORM:
import { identifier, pgIndex } from "@infrastructure/repositories/references";
import { pgTable, varchar, number } from "drizzle-orm/pg-core";
const columns = {
name: varchar("name", { length: 50 }).notNull(),
price: number("price", { length: 100 }),
description: varchar("description", { length: 400 }).unique().notNull(),
};
const product = pgTable(
"product",
{ ...columns, ...identifier },
(table) => pgIndex("product", table, ["name", "email"])
);
type product = typeof product.$inferSelect;
export default product;Key Points:
columns: Custom fields for your domainidentifier: Adds id, createdAt, updatedAt, activated (from infrastructure)pgIndex: Creates database index for performancesrc/domain/product/schema.ts) Zod schemas for runtime validation:
import { withPagination, zodIdentifier } from "@infrastructure/repositories/references";
import { headers } from "@infrastructure/server/interface";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { array, object } from "zod/v4";
import product from "./entity";
// Generate base schemas from Drizzle entity
const create = createInsertSchema(product, {
name: (schema) => schema.min(1).max(50),
price: (schema) => schema.min(1),
description: (schema) => schema.min(1).max(400),
});
const select = createSelectSchema(product, {
...zodIdentifier,
}).partial();
// Define action-specific schemas
const actions = {
headers,
id: select.pick({ id: true }).required(),
read: object({
...select.omit({ id: true }).shape,
...withPagination.shape,
}),
create: create.omit({ id: true }),
update: create.omit({ id: true }).partial(),
delete: select.pick({ id: true }),
};
export default { actions, entity: array(select) };Key Points:
createInsertSchema: Generates Zod schema from Drizzle tablesrc/domain/product/routes.ts) Fastify routes with OpenAPI documentation:
import request from "@infrastructure/server/request";
import type { FastifyInstance } from "fastify";
import deleteEntity from "./actions/delete-entity";
import getById from "./actions/get-by-id";
import getFindByParams from "./actions/get-find-by-params";
import postNewEntity from "./actions/post-new-entity";
import putUpdateEntity from "./actions/put-update-entity";
import schema from "./schema";
export default async function productRoutes(api: FastifyInstance) {
api.get("/ping", { schema: { tags: ["Product"] } }, (_, reply) => reply.code(200).send());
api.get(
"/:id",
{
schema: {
tags: ["Product"],
summary: "Find product by id",
params: schema.actions.id,
headers: schema.actions.headers,
response: { 200: schema.entity, ...request.reply.schemas },
},
},
request.restricted(getById),
);
api.post(
"/",
{
schema: {
tags: ["Product"],
summary: "Create new product",
body: schema.actions.create,
headers: schema.actions.headers,
response: { 201: schema.entity, ...request.reply.schemas },
},
},
request.restricted(postNewEntity),
);
// ... PUT and DELETE routes
}Key Points:
schema.tags: Groups endpoints in Swagger UIschema.params/body/headers: Automatic validationrequest.restricted(): Applies authentication middlewareresponse: Defines response schema for documentationsrc/domain/product/actions/post-new-entity.ts) Create product action with validation and caching:
import cache from "@infrastructure/cache/actions";
import { hash, tag } from "@infrastructure/repositories/references";
import repository from "@infrastructure/repositories/repository";
import { container } from "@infrastructure/server/request";
import identity from "../entity";
import { default as schema } from "../schema";
import getById from "./get-by-id";
export default async function postNewEntity(request: container) {
request.status(201);
// 1. Validate request body
const validRequest = await schema.actions.create.safeParseAsync(request.body());
if (!validRequest.success) {
throw request.badRequest(request.language(), "post/product/{params}");
}
const content = await repository
.insert(user)
.values({
...validRequest.data,
})
.onConflictDoNothing()
.returning();
// 3. Check if insert succeeded
if (!content.length) {
throw request.unprocessableEntity(request.language(), `post/product/${validRequest.data.email}`);
}
// 4. Invalidate cache (all product list queries)
await cache.json.del(tag("product", "find*"));
// 5. Return created product (fetched to ensure cache consistency)
return getById(new container({ params: { id: content[0].id } }));
}Let's trace a complete request:
Request:
curl -X POST http://localhost:3000/api/v1/products \
-H "Content-Type: application/json" \
-H "Authorization: Bearer token" \
-d '{
"name": "John",
"lastName": "Doe",
"email": "john@example.com",
"password": "SecurePass123"
}'What Happens:
userRoutesschema.actions.createrequest.restricted() checks tokenpostNewEntity()hash() encrypts passwordResponse:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "John",
"lastName": "Doe",
"email": "john@example.com",
"activated": true,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
}Request:
curl http://localhost:3000/api/v1/products/123e4567-e89b-12d3-a456-426614174000 \
-H "Authorization: Bearer token"Flow with Caching:
Code (get-by-id.ts):
export default async function getById(request: container) {
request.status(200);
// Validate ID parameter
const validRequest = await schema.actions.id.safeParseAsync(request.params());
if (!validRequest.success) {
throw request.badRequest(request.language(), tag("product", "find{id}"));
}
const { id } = validRequest.data;
const reference = tag("product", "find{id}", { id });
// Check cache first
const cached = await cache.json.get<{ [key: string]: typeof user[] }>(reference);
if (cached?.[reference]) return cached[reference];
// Query database if not cached
const prepare = repository
.select()
.from(user)
.where(eq(user.id, sql.placeholder("id")))
.limit(1)
.orderBy(desc(user.id))
.prepare("/user/id");
const content = await prepare.execute({ id });
if (!content.length) {
throw request.notFound(request.language(), tag("product", "find{id}"));
}
// Cache for 10 minutes
await cache.json.set(reference, content, 60 * 10);
return content;
}Edit entity.ts:
const columns = {
name: varchar("name", { length: 50 }).notNull(),
price: decimal("price", { precision: 10, scale: 2 }).notNull(),
description: varchar("description", { length: 400 }),
// Add new fields
stock: integer("stock").notNull(),
category: varchar("category", { length: 20 }).notNull(),
perishable: boolean("perishable").notNull(),
};Edit schema.ts:
const create = createInsertSchema(user, {
name: (schema) => schema.min(1).max(50),
price: (schema) => schema.min(0),
description: (schema) => schema.max(400),
// Add custom validation
stock: (schema) => schema.min(0),
category: (schema) => schema.enum(["Fruits", "Vegetables", "Meats", "Hygiene"]),
});After modifying the entity:
# Generate migration
bun db:migrate
# Apply to database
bun db:migrate:push# Generate migration
npm run db:migrate
# Apply to database
npm run db:migrate:push# Generate migration
yarn db:migrate
# Apply to database
yarn db:migrate:push# Generate migration
pnpm db:migrate
# Apply to database
pnpm db:migrate:pushWrite tests for your actions:
// tests/unit/domain/product/product-create.spec.ts
import { describe, it, expect, beforeEach } from "bun:test";
import { createProductBuilder } from "@tests/builders/product.builder";
import postNewEntity from "@domain/product/actions/post-new-entity";
describe("Product Creation", () => {
it("should create product with hashed password", async () => {
const data = { name: "Product", price: 10, description: "Description", stock: 10, category: "Fruits", perishable: true }
const productData = createProductBuilder(data);
const result = await postNewEntity(mockRequest(productData));
expect(result[0].name).toBe("Product");
expect(result[0].category).toBe("Fruits");
expect(result[0].perishable).toBe(true);
});
it("should reject invalid email", async () => {
const data = { name: "Product", price: 10, description: "Description", stock: 10, category: "Fruits", perishable: true }
const productData = createProductBuilder(data);
await expect(postNewEntity(mockRequest(productData))).rejects.toThrow();
});
});This example shows:
See Also: