API Rest: Fang JS + Drizzle ORM
¿Quieres ver de lo que es capaz Fang JS cuando se ensucia las manos? En este ejemplo, construiremos una API Rest básica, que nos permita hacer un CRUD de productos, para ello usaremos la arquitectura recomendada. En este ejemplo veremos:
- Persistencia: Drizzle ORM sobre una base de datos PostgreSQL/SQLite.
- Validación: Esquemas de Zod importados desde
@fang-js/fang/zod. - Arquitectura: Separación clara entre Controladores y Servicios.
Requisitos Previos
Section titled “Requisitos Previos”Antes de empezar, asegúrate de tener instalado Drizzle en tu proyecto:
npm i drizzle-orm @libsql/client dotenvnpm i -D drizzle-kit tsxImplementación
Section titled “Implementación”A continuación, se muestra la implementación de la API. Observa cómo el Context fluye a través de los servicios y cómo se usan los métodos del context y las excepciones.
Arquitectura del proyecto
Section titled “Arquitectura del proyecto”├── 📁 src│ ├── 📁 config│ │ └── 📄 keys.ts│ ├── 📁 database│ │ └── 📄 schema.ts│ ├── 📁 lib│ │ └── 📄 drizzle.ts│ ├── 📁 product│ │ ├── 📄 product-controller.ts│ │ ├── 📄 product-scheme.ts│ │ └── 📄 product-service.ts│ └── 📄 index.ts├── 📄 dev.db├── 📄 drizzle.config.ts├── ⚙️ package-lock.json├── ⚙️ package.json└── ⚙️ tsconfig.jsonArchivo principal (index.ts)
Section titled “Archivo principal (index.ts)”import { Fang } from "@fang-js/fang";import { ProductController } from "./product/product-controller.js";import { NotFoundException } from "@fang-js/fang/exceptions";import { cors } from "@fang-js/fang/middlewares";import { methods } from "@fang-js/fang/types";
const app = new Fang();
app.use( cors({ origin: "*", methods: [methods.GET, methods.POST, methods.PUT, methods.DELETE], }),);
app.get("/", (ctx) => { ctx.ok({ message: "Hello World from CRUD fang!" });});
app.group("/product", ProductController.register);
app.onError((error, ctx) => { if (error instanceof NotFoundException) return ctx.notFound({ message: error.message });
return ctx.internalError({ message: error.message });});
app.listen();Controlador de Productos
Section titled “Controlador de Productos”import { Controller, RouteGroup } from "@fang-js/fang";import { ProductService } from "./product-service.js";
@Controllerexport class ProductController { static register(group: RouteGroup) { const productService = new ProductService();
group.get("/", productService.getProducts); group.get("/:id", productService.getProductById); group.post("/", productService.createProduct); group.put("/:id", productService.updateProduct); group.delete("/:id", productService.deleteProduct); }}Esquema de Productos
Section titled “Esquema de Productos”import { z } from "@fang-js/fang/zod";
export const productBodyScheme = z.object({ name: z.string(), price: z.number(), description: z.string().max(255), stock: z.number(),});Servicio de Productos
Section titled “Servicio de Productos”import { BaseService, Context, Service } from "@fang-js/fang";import db from "../lib/drizzle.js";import { productTable } from "../database/schema.js";import { productBodyScheme } from "./product-scheme.js";import { eq } from "drizzle-orm/sql";import { NotFoundException } from "@fang-js/fang/exceptions";
@Serviceexport class ProductService extends BaseService { async getProducts(ctx: Context) { const products = await db.select().from(productTable); ctx.ok({ products }); }
async getProductById(ctx: Context) { const { id } = ctx.params; const [product] = await db .select() .from(productTable) .where(eq(productTable.id, Number(id)));
if (!product) throw new NotFoundException(`Product with ID ${id} not found`);
ctx.ok({ product }); }
async createProduct(ctx: Context) { const { name, description, price, stock } = await ctx.body(productBodyScheme);
const [newProduct] = await db .insert(productTable) .values({ name, description, price, stock, }) .returning();
ctx.created({ message: "Product created successfully!", product: newProduct, }); }
async updateProduct(ctx: Context) { const { id } = ctx.params; const body = await ctx.body(productBodyScheme);
const [updatedProduct] = await db .update(productTable) .set({ ...body, }) .where(eq(productTable.id, Number(id))) .returning();
if (!updatedProduct) throw new NotFoundException(`Product with ID ${id} not found`);
ctx.json({ message: "Product updated successfully!", product: updatedProduct, }); }
async deleteProduct(ctx: Context) { const { id } = ctx.params;
const [deletedProduct] = await db .delete(productTable) .where(eq(productTable.id, Number(id))) .returning();
if (!deletedProduct) throw new NotFoundException(`Product with ID ${id} not found`);
ctx.json({ message: "Product deleted successfully!", product: deletedProduct, }); }}Configuración de Drizzle
Section titled “Configuración de Drizzle”import { drizzle } from "drizzle-orm/libsql";import { createClient } from "@libsql/client";import { DB_FILE_NAME } from "../config/keys.js";
const client = createClient({ url: DB_FILE_NAME });const db = drizzle({ client });
export default db;