{"id":30047898,"url":"https://github.com/emersonlaurentino/norte","last_synced_at":"2025-08-07T10:03:41.258Z","repository":{"id":307081117,"uuid":"1025768246","full_name":"emersonlaurentino/norte","owner":"emersonlaurentino","description":"A modern, type-safe API framework for building production-ready REST APIs with built-in authentication, automatic OpenAPI docs, and domain-driven CRUD operations.","archived":false,"fork":false,"pushed_at":"2025-07-29T10:44:33.000Z","size":144,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-07-29T11:12:49.804Z","etag":null,"topics":["api-framework","crud","minimal-boilerplate","multi-tenant","production-ready","scalar-ui","session-management","typescript","web-framework","zod-validation"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/emersonlaurentino.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-07-24T19:12:00.000Z","updated_at":"2025-07-29T11:12:06.000Z","dependencies_parsed_at":"2025-07-29T11:12:58.062Z","dependency_job_id":"1e4fe139-533b-44ff-a7b8-6991c4522f87","html_url":"https://github.com/emersonlaurentino/norte","commit_stats":null,"previous_names":["emersonlaurentino/norte"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/emersonlaurentino/norte","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emersonlaurentino%2Fnorte","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emersonlaurentino%2Fnorte/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emersonlaurentino%2Fnorte/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emersonlaurentino%2Fnorte/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/emersonlaurentino","download_url":"https://codeload.github.com/emersonlaurentino/norte/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emersonlaurentino%2Fnorte/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268077774,"owners_count":24192176,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-31T02:00:08.723Z","response_time":66,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-framework","crud","minimal-boilerplate","multi-tenant","production-ready","scalar-ui","session-management","typescript","web-framework","zod-validation"],"created_at":"2025-08-07T10:01:22.908Z","updated_at":"2025-08-07T10:03:41.242Z","avatar_url":"https://github.com/emersonlaurentino.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Norte\n\nA modern, type-safe API framework that simplifies building production-ready REST APIs with built-in authentication, automatic OpenAPI documentation, and CRUD operations.\n\n## ✨ Features\n\n- 🚀 **Fast Development** - Build APIs with minimal boilerplate\n- 🔐 **Authentication Ready** - Built-in session management with Better Auth\n- 📚 **Auto Documentation** - Automatic OpenAPI/Swagger generation with Scalar UI\n- 🛡️ **Type Safety** - Full TypeScript support with Zod validation\n- 🔧 **CRUD Made Easy** - Chainable methods for common operations\n- ⚡ **High Performance** - Built on top of Hono for maximum speed\n- 🎯 **Opinionated** - Sensible defaults that just work\n- 🔄 **Error Handling** - Built-in NorteError system with proper HTTP status codes\n\n## 🚀 Quick Start\n\n### Installation\n\n```bash\nbun add norte better-auth\n# or\nnpm install norte better-auth\n# or\nyarn add norte better-auth\n# or\npnpm add norte better-auth\n```\n\n### Basic Usage\n\n```typescript\nimport { Norte, Router, z, NorteError } from 'norte'\n\n// 1. Create your main app\nconst app = new Norte({\n  title: 'My API',\n  version: '1.0.0',\n  authConfig: {\n    // Your Better Auth configuration\n    database: db,\n    emailAndPassword: { enabled: true },\n  }\n})\n\n// 2. Define your response schema\nconst selectSchema = z.object({\n  id: z.string().cuid2(),\n  name: z.string(),\n  email: z.string().email(),\n  age: z.number().min(18),\n  createdAt: z.date()\n})\n\nconst insertSchema = z.object({\n  name: z.string(),\n  email: z.string().email(),\n  age: z.number().min(18)\n})\n\n// 3. Create a router with CRUD operations using domain-driven approach\nconst userRouter = new Router('users', { schema: selectSchema })\n  .list(async () =\u003e {\n    return await getUsersFromDB()\n  })\n  .create({ input: insertSchema }, async ({ input }) =\u003e {\n    return await createUser(input)\n  })\n  .read(async ({ param }) =\u003e {\n    const foundUser = await getUserById(param.userId)\n    if (!foundUser) {\n      return new NorteError('NOT_FOUND', 'User not found')\n    }\n    return foundUser\n  })\n  .update(\n    { input: insertSchema.partial() },\n    async ({ input, param }) =\u003e {\n      const updatedUser = await updateUser(param.userId, input)\n      if (!updatedUser) {\n        return new NorteError('NOT_FOUND', 'User not found')\n      }\n      return updatedUser\n    }\n  )\n  .delete(async ({ param }) =\u003e {\n    const deleted = await deleteUser(param.userId)\n    if (!deleted) {\n      return new NorteError('NOT_FOUND', 'User not found')\n    }\n  })\n\n// 4. Register the router\napp.register(userRouter)\n\n// 5. Export for your runtime\nexport default app\n```\n\n## 📋 API Reference\n\n### Norte Class\n\nThe main application class that handles setup and configuration.\n\n```typescript\nconst app = new Norte({\n  title: string,              // API title for documentation\n  version?: string,           // API version (default: \"1.0.0\")\n  authConfig: BetterAuthOptions  // Better Auth configuration\n})\n```\n\n#### Methods\n\n- `app.middleware(...middlewares)` - Add Hono middleware\n- `app.register(router)` - Register a Router instance\n- `app.fetch` - The fetch handler for your runtime (with proxy support)\n\n### Router Class\n\nDomain-driven API for creating CRUD operations with automatic OpenAPI documentation.\n\n```typescript\n// Root domain\nconst router = new Router(domain: string, config: {\n  schema: ZodSchema    // Response data schema\n})\n\n// Nested domain\nconst router = new Router(parent: Router, domain: string, config: {\n  schema: ZodSchema\n})\n```\n\n#### Domain-Driven Design\n\nNorte uses domain names to automatically generate paths, parameters, and OpenAPI tags:\n\n```typescript\n// Domain: 'stores' -\u003e generates /stores, parameter 'storeId', and OpenAPI tag 'Stores'\nconst storeRouter = new Router('stores', {\n  schema: storeSchema\n})\n\n// Domain: 'products' nested under stores -\u003e generates /stores/:storeId/products and tag 'Products'\nconst productsRouter = new Router(storeRouter, 'products', {\n  schema: productSchema\n})\n\n// Domain: 'variants' nested under products -\u003e generates /stores/:storeId/products/:productId/variants and tag 'Variants'\nconst variantsRouter = new Router(productsRouter, 'variants', {\n  schema: variantSchema\n})\n```\n\n#### Auto-Generated Routes\n\nEach domain automatically generates RESTful routes and OpenAPI tags:\n\n| Domain | Generated Routes | OpenAPI Tag |\n|--------|------------------|-------------|\n| `stores` | `GET /stores`, `POST /stores`, `GET /stores/:id`, `PUT /stores/:id`, `DELETE /stores/:id` | `Stores` |\n| `products` (nested) | `GET /stores/:storeId/products`, `POST /stores/:storeId/products`, etc. | `Products` |\n| `variants` (nested) | `GET /stores/:storeId/products/:productId/variants`, etc. | `Variants` |\n\n#### Parameter Auto-Generation\n\nParameters and OpenAPI tags are automatically generated from domain names:\n\n```typescript\n// Domain transformations:\n'stores' -\u003e 'storeId' (parameter) + 'Stores' (OpenAPI tag)\n'products' -\u003e 'productId' (parameter) + 'Products' (OpenAPI tag)\n'categories' -\u003e 'categoryId' (parameter) + 'Categories' (OpenAPI tag)\n'variants' -\u003e 'variantId' (parameter) + 'Variants' (OpenAPI tag)\n\n// Handlers automatically receive all parent parameters + current domain parameter\nvariantsRouter.read(async ({ param }) =\u003e {\n  // param contains: { storeId, productId, variantId }\n  const variant = await getVariant(param.variantId, param.productId, param.storeId)\n  return variant\n})\n```\n\n#### CRUD Methods\n\nEach method is chainable and generates the appropriate OpenAPI route:\n\n**List Resources**\n```typescript\n// Simple usage\n.list(handler: ListHandler)\n\n// With configuration\n.list(config: RouteCommonConfig, handler: ListHandler)\n```\n\n**Create Resource**\n```typescript\n.create(\n  config: RouteCommonConfig \u0026 { input: ZodSchema },\n  handler: InsertHandler\n)\n```\n\n**Read Resource**\n```typescript\n// Simple usage\n.read(handler: ReadHandler)\n\n// With configuration\n.read(config: RouteCommonConfig, handler: ReadHandler)\n```\n\n**Update Resource**\n```typescript\n.update(\n  config: RouteCommonConfig \u0026 { input: ZodSchema },\n  handler: UpdateHandler\n)\n```\n\n**Delete Resource**\n```typescript\n// Simple usage\n.delete(handler: DeleteHandler)\n\n// With configuration\n.delete(config: RouteCommonConfig, handler: DeleteHandler)\n```\n\n#### Handler Types\n\n```typescript\ntype HandlerResult\u003cT\u003e = Promise\u003cT | NorteError\u003e | T | NorteError\n\ntype HandlerContext\u003c\n  TParams extends Record\u003cstring, string\u003e = Record\u003cstring, never\u003e,\n\u003e = {\n  session: Session | null\n  user: User | null\n  param: TParams\n  request: NorteRequest\n}\n\ntype ListHandler\u003c\n  TResponse extends ZodSchema,\n  TParams extends Record\u003cstring, string\u003e,\n\u003e = (c: HandlerContext\u003cTParams\u003e) =\u003e HandlerResult\u003cz.infer\u003cTResponse\u003e[]\u003e\n\ntype InsertHandler\u003c\n  TInput extends ZodSchema,\n  TResponse extends ZodSchema,\n  TParams extends Record\u003cstring, string\u003e,\n\u003e = (\n  c: HandlerContext\u003cTParams\u003e \u0026 { input: z.infer\u003cTInput\u003e },\n) =\u003e HandlerResult\u003cz.infer\u003cTResponse\u003e\u003e\n\ntype ReadHandler\u003c\n  TResponse extends ZodSchema,\n  TParams extends Record\u003cstring, string\u003e,\n\u003e = (c: HandlerContext\u003cTParams\u003e) =\u003e HandlerResult\u003cz.infer\u003cTResponse\u003e\u003e\n\ntype UpdateHandler\u003c\n  TInput extends ZodSchema,\n  TResponse extends ZodSchema,\n  TParams extends Record\u003cstring, string\u003e,\n\u003e = (\n  c: HandlerContext\u003cTParams\u003e \u0026 { input: z.infer\u003cTInput\u003e },\n) =\u003e HandlerResult\u003cz.infer\u003cTResponse\u003e\u003e\n\ntype DeleteHandler\u003cTParams extends Record\u003cstring, string\u003e\u003e = (\n  c: HandlerContext\u003cTParams\u003e,\n) =\u003e HandlerResult\u003cundefined\u003e\n```\n\n#### Configuration Options\n\n```typescript\ninterface RouteCommonConfig {\n  isPublic?: boolean  // Skip authentication (default: false)\n}\n```\n\n## 🏗️ Nested Domains\n\nCreate nested resource hierarchies using domain-driven design:\n\n### Basic Nested Domains\n\n```typescript\nimport { Router, z, NorteError } from 'norte'\n\n// Root domain\nconst storeRouter = new Router('stores', {\n  schema: storeSchema\n})\n  .list(async ({ user }) =\u003e {\n    const stores = await getStoresByUser(user.id)\n    return stores\n  })\n\n// Nested domain - parent as first argument\nconst productsRouter = new Router(storeRouter, 'products', {\n  schema: productSchema\n})\n  .list(async ({ param }) =\u003e {\n    // param.storeId is automatically available from parent domain\n    const products = await getProductsByStore(param.storeId)\n    return products\n  })\n  .read(async ({ param }) =\u003e {\n    // param contains both storeId and productId\n    const product = await getProductById(param.productId, param.storeId)\n    return product || new NorteError('NOT_FOUND', 'Product not found')\n  })\n\n// Register both routers\napp.register(storeRouter)\napp.register(productsRouter)\n```\n\n### Deep Domain Nesting\n\n```typescript\n// Four-level domain hierarchy\nconst storeRouter = new Router('stores', { \n  schema: storeSchema \n})\n\nconst productsRouter = new Router(storeRouter, 'products', { \n  schema: productSchema \n})\n\nconst variantsRouter = new Router(productsRouter, 'variants', { \n  schema: variantSchema \n})\n\nconst optionsRouter = new Router(variantsRouter, 'options', { \n  schema: optionSchema \n})\n\n// Final routes: /stores/:storeId/products/:productId/variants/:variantId/options\n// Handler receives: { storeId, productId, variantId, id }\n```\n\n\n\n### Domain Constructor Patterns\n\n```typescript\n// Root domain\nnew Router(domain: string, config: RouterConfig)\n\n// Nested domain  \nnew Router(parent: Router, domain: string, config: RouterConfig)\n```\n\n**Note**: The `name` attribute is no longer needed in the config. OpenAPI tags and route names are automatically generated from the domain name (e.g., `'stores'` becomes `'Stores'`).\n\n### Parameter Inheritance\n\nNested routers automatically inherit all parameters from their parent chain:\n\n```typescript\n// For nested domain: pharmacies -\u003e categories -\u003e products -\u003e variants\ninterface NestedParams {\n  pharmacyId: string    // From parent 'pharmacies' domain\n  categoryId: string    // From parent 'categories' domain  \n  productId: string     // From parent 'products' domain\n  variantId: string     // From current 'variants' domain\n}\n\n// All parameters are automatically validated as CUID2 strings\nconst paramSchema = z.object({\n  pharmacyId: z.string().cuid2(),\n  categoryId: z.string().cuid2(),\n  productId: z.string().cuid2(),\n  variantId: z.string().cuid2()\n})\n```\n\n## 🚨 Error Handling\n\nNorte includes a comprehensive error system with the `NorteError` class:\n\n```typescript\nimport { NorteError } from 'norte'\n\n// Available error codes\ntype ErrorCode = \n  | 'NOT_FOUND'           // 404\n  | 'INVALID_INPUT'       // 400\n  | 'UNAUTHORIZED'        // 401\n  | 'FORBIDDEN'           // 403\n  | 'CONFLICT'            // 409\n  | 'INTERNAL_SERVER_ERROR' // 500\n\n// Usage in handlers\nrouter.read(async ({ param }) =\u003e {\n  const user = await getUserById(param.userId)\n  if (!user) {\n    return new NorteError('NOT_FOUND', 'User not found', { userId: param.userId })\n  }\n  return user\n})\n```\n\n## 🔐 Authentication\n\nNorte includes built-in authentication powered by Better Auth:\n\n### Protected Routes (Default)\n\n```typescript\n// This route requires authentication\nrouter.list(async ({ session, user }) =\u003e {\n  // session and user are available and not null\n  const users = await getUsersForTenant(user.id)\n  return users\n})\n```\n\n### Public Routes\n\n```typescript\n// This route is publicly accessible\nrouter.list({ isPublic: true }, async ({ session, user }) =\u003e {\n  // session and user might be null\n  const publicUsers = await getPublicUsers()\n  return publicUsers\n})\n```\n\n### Authentication Endpoints\n\nNorte automatically sets up authentication endpoints at `/auth/**`:\n\n- `POST /auth/sign-in` - Sign in\n- `POST /auth/sign-up` - Sign up  \n- `POST /auth/sign-out` - Sign out\n- `GET /auth/session` - Get current session\n- And more from Better Auth...\n\n## 📚 Documentation\n\nNorte automatically generates interactive API documentation using Scalar:\n\n- **Main docs**: Visit `/` for multi-source Scalar documentation\n- **API docs**: Available at `/docs` (OpenAPI 3.1)\n- **Auth docs**: Authentication endpoints at `/auth/open-api/generate-schema`\n- **Health check**: Available at `/healthcheck`\n\nThe documentation includes:\n- Automatic schema generation from Zod schemas\n- Request/response examples\n- Authentication requirements\n- Error response formats\n\n## 🛠️ Advanced Usage\n\n### Custom Middleware\n\nNorte comes with `logger` and `prettyJSON` middlewares configured by default. You can add any additional Hono middleware through the `middleware()` method:\n\n```typescript\nimport { cors } from 'hono/cors'\nimport { compress } from 'hono/compress'\n\n// Add CORS middleware\napp.middleware(cors({\n  origin: ['https://yourdomain.com'],\n  credentials: true\n}))\n\n// Add compression middleware\napp.middleware(compress())\n\n// Custom middleware\napp.middleware(async (c, next) =\u003e {\n  console.log('Custom middleware executed')\n  await next()\n})\n```\n\n### Parameter Validation\n\nAll ID parameters are automatically validated as CUID2 strings:\n\n```typescript\n// Automatically validates param.userId as z.cuid2()\nuserRouter.read(async ({ param }) =\u003e {\n  const { userId } = param // userId is guaranteed to be a valid CUID2\n  // ...\n})\n```\n\n### Response Format\n\nAll successful responses follow a consistent format:\n\n```typescript\n// List responses\n{ \"data\": [...] }\n\n// Single resource responses  \n{ \"data\": {...} }\n\n// Error responses\n{ \n  \"error\": \"ERROR_CODE\",\n  \"message\": \"Human readable message\",\n  \"details\": {...} // Optional additional details\n}\n```\n\n## 🎯 Examples\n\n### Database Integration (Drizzle) with Domains\n\n```typescript\nimport { eq } from 'drizzle-orm'\nimport { createInsertSchema, createSelectSchema } from 'drizzle-zod'\n\nconst userResponseSchema = createSelectSchema(userTable)\nconst insertSchema = createInsertSchema(userTable).omit({ \n  id: true, \n  createdAt: true, \n  updatedAt: true \n})\n\n// Domain-driven router instead of path-based\nconst userRouter = new Router('users', {\n  schema: userResponseSchema\n})\n  .list(async ({ user }) =\u003e {\n    const users = await db.select().from(userTable).where(eq(userTable.tenantId, user.tenantId))\n    return users\n  })\n  .create(\n    { input: insertSchema },\n    async ({ input, user }) =\u003e {\n      const [newUser] = await db\n        .insert(userTable)\n        .values({ ...input, tenantId: user.tenantId })\n        .returning()\n      return newUser\n    }\n  )\n  .read(async ({ param }) =\u003e {\n    const [user] = await db\n      .select()\n      .from(userTable)\n      .where(eq(userTable.id, param.userId))\n    \n    if (!user) {\n      return new NorteError('NOT_FOUND', 'User not found')\n    }\n    \n    return user\n  })\n```\n\n### Multi-Tenant Store Example\n\n```typescript\n// Store domain for multi-tenant architecture\nconst storeRouter = new Router('stores', {\n  schema: storeSchema\n})\n  .list(async ({ user }) =\u003e {\n    // Get stores for current tenant\n    const stores = await db\n      .select()\n      .from(storeTable)\n      .where(eq(storeTable.tenantId, user.tenantId))\n    return stores\n  })\n\n// Orders nested under stores - generates /stores/:storeId/orders\nconst ordersRouter = new Router(storeRouter, 'orders', {\n  schema: orderSchema\n})\n  .list(async ({ param, user }) =\u003e {\n    // param.storeId automatically available with validation\n    const orders = await db\n      .select()\n      .from(orderTable)\n      .where(\n        and(\n          eq(orderTable.storeId, param.storeId),\n          eq(orderTable.tenantId, user.tenantId) // Multi-tenant security\n        )\n      )\n    return orders\n  })\n\n// Items nested under orders - generates /stores/:storeId/orders/:orderId/items\nconst orderItemsRouter = new Router(ordersRouter, 'items', {\n  schema: orderItemSchema\n})\n  .list(async ({ param }) =\u003e {\n    // param contains: { storeId, orderId }\n    const items = await db\n      .select()\n      .from(orderItemTable)\n      .where(eq(orderItemTable.orderId, param.orderId))\n    return items\n  })\n```\n\n### Validation with Custom Error Messages\n\n```typescript\nconst createPostSchema = z.object({\n  title: z.string().min(1, 'Title is required').max(100, 'Title too long'),\n  content: z.string().min(10, 'Content must be at least 10 characters'),\n  published: z.boolean().default(false)\n})\n\nconst postRouter = new Router('posts', {\n  schema: postResponseSchema\n})\n  .create(\n    { input: createPostSchema },\n    async ({ input, user }) =\u003e {\n      // Input is automatically validated against createPostSchema\n      const post = await createPost({ ...input, authorId: user.id })\n      return post\n    }\n  )\n```\n\n## 🤝 Contributing\n\nWe welcome contributions! Please see our contributing guide for details.\n\n## 📄 License\n\nMIT © Emerson Laurentino\n\n## 🔗 Links\n\n- [Better Auth](https://better-auth.com)\n- [Hono](https://hono.dev)\n- [Zod](https://zod.dev)\n- [Scalar](https://scalar.com)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Femersonlaurentino%2Fnorte","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Femersonlaurentino%2Fnorte","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Femersonlaurentino%2Fnorte/lists"}