{"_id":"@ambrosia-unce/http","_rev":"2-acd67584e75dfa154fc02bb7ec34612a","name":"@ambrosia-unce/http","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@ambrosia-unce/http","version":"1.0.0","keywords":["http","web-framework","decorators","guards","interceptors","pipes","middleware","typescript","bun","ambrosia"],"author":{"name":"Ambrosia Framework"},"license":"MIT","_id":"@ambrosia-unce/http@1.0.0","maintainers":[{"name":"funnybunny_unce","email":"funnybunny.unce@gmail.com"}],"homepage":"https://github.com/ambrosia-unce/ambrosia/tree/master/packages/http","bugs":{"url":"https://github.com/ambrosia-unce/ambrosia/issues"},"dist":{"shasum":"e434aeaa12b3bc57c84a06d074a921cfb0ca42ac","tarball":"https://registry.npmjs.org/@ambrosia-unce/http/-/http-1.0.0.tgz","fileCount":138,"integrity":"sha512-5Ls/EDiGvQXmWk2mXoE3aPTyf6AMa7WAkTNo239x2TZgnXxxlkvzhI3Orbr9kynmdnHk1b9ziJnr20gocA9lGA==","signatures":[{"sig":"MEUCIQCGhV+I63p+M6p5SICeqxJM3LKfVkoMpOY70vP5sRONmAIgGhNhFdMAC1riKGDLOutleN+bFj91MnvVgtkLxEgXmHU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@ambrosia-unce%2fhttp@1.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":225969},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"bun":">=1.3.6"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"d86ee39512a468e7c5413e9c243c07849306455e","scripts":{"test":"bun test","build":"bun run build.ts","test:unit":"bun test tests/unit","typecheck":"tsc --noEmit","test:watch":"bun test --watch","test:coverage":"bun test --coverage","prepublishOnly":"bun run typecheck && bun run build","test:integration":"bun test tests/integration"},"_npmUser":{"name":"funnybunny_unce","email":"funnybunny.unce@gmail.com"},"repository":{"url":"git+https://github.com/ambrosia-unce/ambrosia.git","type":"git","directory":"packages/http"},"_npmVersion":"10.8.2","description":"Provider-agnostic HTTP layer with decorators, guards, interceptors, pipes and middleware for the Ambrosia framework","directories":{},"_nodeVersion":"20.20.1","dependencies":{"reflect-metadata":"^0.2.1","@sinclair/typebox":"^0.34.48","@ambrosia-unce/core":"workspace:*"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"@types/bun":"latest","@types/node":"^25.1.0"},"peerDependencies":{"typescript":"^5"},"_npmOperationalInternal":{"tmp":"tmp/http_1.0.0_1774566993182_0.3282922372714634","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@ambrosia-unce/http","version":"1.0.1","description":"Provider-agnostic HTTP layer with decorators, guards, interceptors, pipes and middleware for the Ambrosia framework","type":"module","license":"MIT","author":{"name":"Ambrosia Framework"},"homepage":"https://github.com/ambrosia-unce/ambrosia/tree/master/packages/http","repository":{"type":"git","url":"git+https://github.com/ambrosia-unce/ambrosia.git","directory":"packages/http"},"bugs":{"url":"https://github.com/ambrosia-unce/ambrosia/issues"},"keywords":["http","web-framework","decorators","guards","interceptors","pipes","middleware","typescript","bun","ambrosia"],"engines":{"bun":">=1.3.6"},"main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts"}},"scripts":{"build":"bun run build.ts","prepublishOnly":"bun run typecheck && bun run build","test":"bun test","test:watch":"bun test --watch","test:coverage":"bun test --coverage","test:unit":"bun test tests/unit","test:integration":"bun test tests/integration","typecheck":"tsc --noEmit"},"devDependencies":{"@types/bun":"latest","@types/node":"^25.1.0"},"peerDependencies":{"typescript":"^5"},"publishConfig":{"access":"public"},"dependencies":{"@ambrosia-unce/core":"^1.0.1","@sinclair/typebox":"^0.34.48","reflect-metadata":"^0.2.1"},"_id":"@ambrosia-unce/http@1.0.1","gitHead":"623e54bd30bbb3e8ce80ad7cedb8fd0cd9daf9ee","_nodeVersion":"20.20.1","_npmVersion":"10.8.2","dist":{"integrity":"sha512-gSeTd2XPNB6Qobr+STDRXNXXS+Z5GT7CICjaAlsR+6KybtzUCH9wRsms3201y3E7HfQj85MVBrBPP3W1jqnkIw==","shasum":"b48564fe510b5317ff7fafd6382bce82b9510e95","tarball":"https://registry.npmjs.org/@ambrosia-unce/http/-/http-1.0.1.tgz","fileCount":138,"unpackedSize":225964,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@ambrosia-unce%2fhttp@1.0.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIDkyqg8WNktdMAJ5+VOS53pgItTOfmWRWV69bHk1ycFDAiEA6ufl4l99FX511MQgELgwMIRevQjwVWFaiZ7pwmCB0Kg="}]},"_npmUser":{"name":"funnybunny_unce","email":"funnybunny.unce@gmail.com"},"directories":{},"maintainers":[{"name":"funnybunny_unce","email":"funnybunny.unce@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/http_1.0.1_1774600003265_0.11167771321716802"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-26T23:16:33.109Z","modified":"2026-03-27T08:26:43.764Z","1.0.0":"2026-03-26T23:16:33.394Z","1.0.1":"2026-03-27T08:26:43.410Z"},"bugs":{"url":"https://github.com/ambrosia-unce/ambrosia/issues"},"author":{"name":"Ambrosia Framework"},"license":"MIT","homepage":"https://github.com/ambrosia-unce/ambrosia/tree/master/packages/http","keywords":["http","web-framework","decorators","guards","interceptors","pipes","middleware","typescript","bun","ambrosia"],"repository":{"type":"git","url":"git+https://github.com/ambrosia-unce/ambrosia.git","directory":"packages/http"},"description":"Provider-agnostic HTTP layer with decorators, guards, interceptors, pipes and middleware for the Ambrosia framework","maintainers":[{"name":"funnybunny_unce","email":"funnybunny.unce@gmail.com"}],"readme":"# @ambrosia-unce/http\n\nProvider-agnostic HTTP layer for the Ambrosia framework. Decorator-based controllers, a pre-compiled request pipeline, and full support for guards, interceptors, pipes, middleware, exception filters, SSE, and OpenAPI generation.\n\n## Features\n\n- **Provider-agnostic** -- swap between adapters by implementing the `HttpProvider` interface\n- **Pre-compiled request pipeline** -- route handlers are compiled once at startup for minimal per-request overhead\n- **Full lifecycle pipeline** -- Middleware, Guards, Interceptors, Pipes, Handler, Exception Filters\n- **Decorator-driven routing** -- `@Controller`, `@Http.Get()`, `@Http.Post()`, `@Body()`, `@Param()`, and more\n- **Built-in pipes** -- `ValidationPipe`, `ParseIntPipe`, `ParseBoolPipe`, `ParseFloatPipe`, `ParseUUIDPipe`, `ParseEnumPipe`, `DefaultValuePipe`\n- **Built-in exceptions** -- `BadRequestException`, `UnauthorizedException`, `ForbiddenException`, `NotFoundException`, and others with standard HTTP status codes\n- **Server-Sent Events** -- `@Sse()` decorator with `SseStream` helper\n- **OpenAPI 3.0 generation** -- automatic spec generation from controller metadata and `@ApiProperty` / `@ApiResponse` / `@ApiTags` decorators\n- **Custom metadata** -- `SetMetadata` / `@Public()` for role-based access control and similar patterns\n- **Request scoping** -- `Scope.REQUEST` providers via `AsyncLocalStorage`\n- **Testing utilities** -- `TestingHttpFactory` with `MockHttpProvider` for full pipeline testing without a real server\n- **TypeScript first** -- full type safety across the entire API surface\n- **Bun native** -- optimized for the Bun runtime\n\n## Installation\n\n```bash\nbun add @ambrosia-unce/http @ambrosia-unce/core reflect-metadata\n```\n\nYou will also need an HTTP provider adapter, for example:\n\n```bash\nbun add @ambrosia-unce/http-elysia\n```\n\n## Quick Start\n\n```typescript\nimport \"reflect-metadata\";\nimport { Injectable } from \"@ambrosia-unce/core\";\nimport {\n  HttpApplication,\n  Controller,\n  Http,\n  Body,\n  Param,\n  Query,\n  Status,\n  type HttpPackDefinition,\n} from \"@ambrosia-unce/http\";\nimport { ElysiaProvider } from \"@ambrosia-unce/http-elysia\";\n\n// Define a service\n@Injectable()\nclass UserService {\n  private users = [{ id: \"1\", name: \"Alice\" }];\n\n  findAll() {\n    return this.users;\n  }\n\n  findOne(id: string) {\n    return this.users.find((u) => u.id === id);\n  }\n\n  create(data: { name: string }) {\n    const user = { id: String(this.users.length + 1), ...data };\n    this.users.push(user);\n    return user;\n  }\n}\n\n// Define a controller\n@Controller(\"/users\")\nclass UserController {\n  constructor(private userService: UserService) {}\n\n  @Http.Get(\"/\")\n  list(@Query(\"search\") search?: string) {\n    const users = this.userService.findAll();\n    return search\n      ? users.filter((u) => u.name.includes(search))\n      : users;\n  }\n\n  @Http.Get(\"/:id\")\n  getOne(@Param(\"id\") id: string) {\n    return this.userService.findOne(id);\n  }\n\n  @Http.Post(\"/\")\n  @Status(201)\n  create(@Body() body: { name: string }) {\n    return this.userService.create(body);\n  }\n}\n\n// Define a pack (module)\nconst UserPack: HttpPackDefinition = {\n  name: \"UserPack\",\n  controllers: [UserController],\n  providers: [{ token: UserService, useClass: UserService }],\n  exports: [UserService],\n};\n\n// Bootstrap the application\nconst app = await HttpApplication.create({\n  provider: ElysiaProvider,\n  packs: [UserPack],\n  prefix: \"/api\",\n});\n\nawait app.listen(3000);\n```\n\n## Key Concepts\n\n### Controllers\n\nControllers handle incoming requests and return responses. The `@Controller()` decorator marks a class as a controller and automatically applies `@Injectable()`.\n\n```typescript\n@Controller(\"/products\")\nclass ProductController {\n  @Http.Get(\"/\")\n  list() {\n    return [{ id: 1, name: \"Widget\" }];\n  }\n\n  @Http.Get(\"/:id\")\n  getOne(@Param(\"id\") id: string) {\n    return { id, name: \"Widget\" };\n  }\n\n  @Http.Post(\"/\")\n  @Status(201)\n  create(@Body() body: any) {\n    return { id: 2, ...body };\n  }\n\n  @Http.Put(\"/:id\")\n  update(@Param(\"id\") id: string, @Body() body: any) {\n    return { id, ...body };\n  }\n\n  @Http.Delete(\"/:id\")\n  @Status(204)\n  remove(@Param(\"id\") id: string) {}\n}\n```\n\n#### Parameter Decorators\n\nExtract data from the incoming request:\n\n| Decorator | Description |\n|---|---|\n| `@Body()` | Parsed request body |\n| `@Query(key?)` | Query string parameters (all or by key) |\n| `@Param(key?)` | Route path parameters (all or by key) |\n| `@Headers()` | All request headers |\n| `@Header(name)` | Single request header by name |\n| `@Req()` | Full request object (`IHttpRequest`) |\n| `@Res()` | Full response object (`IHttpResponse`) |\n| `@Ctx()` | Native provider context (e.g. Elysia `Context`) |\n| `@Cookie(name?)` | Cookies (all or by name) |\n| `@Ip()` | Client IP address |\n| `@Session()` | Session data |\n| `@UploadedFile(field)` | Single uploaded file by field name |\n| `@UploadedFiles(field?)` | Array of uploaded files (all or by field) |\n\n#### Response Decorators\n\n| Decorator | Description |\n|---|---|\n| `@Status(code)` | Set HTTP status code |\n| `@SetHeader(name, value)` | Set a response header |\n| `@Redirect(url, status?)` | Redirect to a URL (default 302) |\n| `@Timeout(ms)` | Set handler timeout; throws `RequestTimeoutException` on expiry |\n| `@Sse()` | Mark endpoint as a Server-Sent Events stream |\n\n### Request Pipeline\n\nEvery request flows through a pre-compiled pipeline:\n\n```\nMiddleware --> Guards --> Interceptors --> [Param resolution + Pipes] --> Handler --> Filters\n```\n\nPipeline components can be applied globally, at the controller level, or at individual method level.\n\n### Guards\n\nGuards determine whether a request should be handled. Return `true` to allow or `false` to deny (throws `ForbiddenException`).\n\n```typescript\n@Injectable()\nclass AuthGuard implements Guard {\n  canActivate(context: ExecutionContext): boolean | Promise<boolean> {\n    const http = context.switchToHttp();\n    const token = http.getRequest().headers[\"authorization\"];\n    return !!token;\n  }\n}\n\n@Controller(\"/admin\")\n@UseGuard(AuthGuard)\nclass AdminController {\n  @Http.Get(\"/dashboard\")\n  dashboard() {\n    return { message: \"Welcome, admin\" };\n  }\n\n  // Skip auth for this route using @Public()\n  @Http.Get(\"/health\")\n  @Public()\n  health() {\n    return { ok: true };\n  }\n}\n```\n\nGuards can read custom metadata via `context.getMetadata(key)`. Use `SetMetadata` or the built-in `@Public()` decorator to attach metadata:\n\n```typescript\nconst Roles = (...roles: string[]) => SetMetadata(\"roles\", roles);\n\n@Injectable()\nclass RoleGuard implements Guard {\n  canActivate(context: ExecutionContext) {\n    const requiredRoles = context.getMetadata(\"roles\") as string[];\n    if (!requiredRoles) return true;\n    // ... check user roles\n  }\n}\n```\n\n### Interceptors\n\nInterceptors wrap the handler execution and can transform the request, the response, or both.\n\n```typescript\n@Injectable()\nclass LoggingInterceptor implements Interceptor {\n  async intercept(context: ExecutionContext, next: CallHandler): Promise<any> {\n    const start = Date.now();\n    const result = await next.handle();\n    console.log(`Request took ${Date.now() - start}ms`);\n    return result;\n  }\n}\n\n@Injectable()\nclass WrapResponseInterceptor implements Interceptor {\n  async intercept(context: ExecutionContext, next: CallHandler): Promise<any> {\n    const data = await next.handle();\n    return { success: true, data, timestamp: new Date().toISOString() };\n  }\n}\n\n@Controller(\"/posts\")\n@UseInterceptor(LoggingInterceptor)\nclass PostController {\n  @Http.Get(\"/\")\n  @UseInterceptor(WrapResponseInterceptor)\n  list() {\n    return [{ id: 1, title: \"Hello\" }];\n  }\n}\n```\n\n### Pipes\n\nPipes validate and transform parameter values before they reach the handler.\n\n```typescript\n// Built-in pipes\n@Controller(\"/items\")\nclass ItemController {\n  @Http.Get(\"/:id\")\n  @UsePipe(ParseIntPipe)\n  getItem(@Param(\"id\") id: number) {\n    // id is already a number\n  }\n}\n\n// Custom pipe\n@Injectable()\nclass TrimStringPipe implements Pipe {\n  transform(value: any): any {\n    return typeof value === \"string\" ? value.trim() : value;\n  }\n}\n```\n\nBuilt-in pipes:\n\n| Pipe | Description |\n|---|---|\n| `ValidationPipe` | Validates input data |\n| `ParseIntPipe` | Converts string to integer |\n| `ParseFloatPipe` | Converts string to float |\n| `ParseBoolPipe` | Converts string to boolean |\n| `ParseUUIDPipe` | Validates UUID format |\n| `ParseEnumPipe` | Validates value is a member of an enum |\n| `DefaultValuePipe` | Provides a default when value is `undefined` or `null` |\n\n### Middleware\n\nMiddleware executes before guards in the pipeline. It supports both class-based and functional styles.\n\n```typescript\n// Class-based middleware\n@Injectable()\nclass CorsMiddleware implements Middleware {\n  async use(req: IHttpRequest, res: IHttpResponse, next: () => Promise<void>) {\n    res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n    await next();\n  }\n}\n\n// Functional middleware\nconst logger: MiddlewareFunction = async (req, res, next) => {\n  console.log(`${req.method} ${req.path}`);\n  await next();\n};\n\n@Controller(\"/api\")\n@UseMiddleware(CorsMiddleware, logger)\nclass ApiController {\n  @Http.Get(\"/ping\")\n  ping() {\n    return \"pong\";\n  }\n}\n```\n\nMiddleware can also be applied globally via `HttpApplicationOptions.globalMiddleware`.\n\n### Exception Filters\n\nException filters catch errors thrown during request processing and produce a structured error response. A default filter is registered automatically if none are specified.\n\n```typescript\n@Injectable()\nclass CustomExceptionFilter implements ExceptionFilter {\n  catch({ exception, httpContext }: ExceptionFilterArgs) {\n    const status = exception.status || 500;\n    return {\n      statusCode: status,\n      message: exception.message,\n      timestamp: new Date().toISOString(),\n      path: httpContext.request.path,\n    };\n  }\n}\n\n@Controller(\"/api\")\n@UseFilter(CustomExceptionFilter)\nclass ApiController {\n  @Http.Get(\"/fail\")\n  fail() {\n    throw new NotFoundException(\"Resource not found\");\n  }\n}\n```\n\nBuilt-in exceptions:\n\n| Exception | Status Code |\n|---|---|\n| `BadRequestException` | 400 |\n| `UnauthorizedException` | 401 |\n| `ForbiddenException` | 403 |\n| `NotFoundException` | 404 |\n| `MethodNotAllowedException` | 405 |\n| `RequestTimeoutException` | 408 |\n| `ConflictException` | 409 |\n| `UnprocessableEntityException` | 422 |\n| `InternalServerErrorException` | 500 |\n\n### Server-Sent Events (SSE)\n\nMark a route with `@Sse()` and return an `SseStream` instance:\n\n```typescript\n@Controller(\"/events\")\nclass EventsController {\n  @Http.Get(\"/\")\n  @Sse()\n  stream() {\n    const sse = new SseStream();\n\n    const interval = setInterval(() => {\n      sse.send({ data: { time: Date.now() }, event: \"tick\" });\n    }, 1000);\n\n    sse.onClose(() => clearInterval(interval));\n\n    return sse;\n  }\n}\n```\n\n### OpenAPI Generation\n\nGenerate an OpenAPI 3.0 specification from your controller metadata:\n\n```typescript\nimport { OpenApiGenerator } from \"@ambrosia-unce/http\";\n\nconst spec = OpenApiGenerator.generate({\n  title: \"My API\",\n  version: \"1.0.0\",\n  description: \"Auto-generated documentation\",\n  prefix: \"/api\",\n});\n\n@Controller(\"/docs\")\nclass DocsController {\n  @Http.Get(\"/openapi.json\")\n  getSpec() {\n    return spec;\n  }\n}\n```\n\nUse `@ApiProperty`, `@ApiResponse`, and `@ApiTags` decorators on your DTOs and controllers for richer specs.\n\n### HttpPackDefinition\n\n`HttpPackDefinition` extends the core `PackDefinition` with a `controllers` array. Use it to organize your application into feature modules:\n\n```typescript\nconst OrderPack: HttpPackDefinition = {\n  name: \"OrderPack\",\n  controllers: [OrderController],\n  providers: [\n    { token: OrderService, useClass: OrderService },\n    { token: OrderRepository, useClass: OrderRepository },\n  ],\n  exports: [OrderService],\n  imports: [UserPack],\n};\n```\n\n### HttpApplication Options\n\n```typescript\nconst app = await HttpApplication.create({\n  provider: ElysiaProvider,        // HTTP provider class (required)\n  packs: [UserPack, OrderPack],    // Feature packs\n  controllers: [HealthController], // Additional standalone controllers\n  prefix: \"/api\",                  // Global route prefix\n  port: 3000,                      // Default listen port\n  globalMiddleware: [CorsMiddleware],\n  globalGuards: [AuthGuard],\n  globalInterceptors: [LoggingInterceptor],\n  globalPipes: [ValidationPipe],\n  globalFilters: [CustomExceptionFilter],\n  excludeControllers: [DebugController], // Exclude specific controllers\n});\n\nawait app.listen();   // Uses port from options, or 3000 by default\nawait app.close();    // Graceful shutdown with pack onDestroy hooks\n```\n\n## Testing\n\n`TestingHttpFactory` creates an application backed by a `MockHttpProvider` so you can exercise the full request pipeline without starting a server:\n\n```typescript\nimport { TestingHttpFactory } from \"@ambrosia-unce/http\";\n\nconst module = await TestingHttpFactory\n  .create({\n    packs: [UserPack],\n    controllers: [HealthController],\n  })\n  .overrideValue(DB_TOKEN, mockDatabase)\n  .compile();\n\n// Simulate a GET request\nconst res = await module.inject({ method: \"GET\", url: \"/users/1\" });\nexpect(res.statusCode).toBe(200);\nexpect(res.body.name).toBe(\"Alice\");\n\n// Simulate a POST request\nconst created = await module.inject({\n  method: \"POST\",\n  url: \"/users\",\n  body: { name: \"Bob\" },\n  headers: { \"content-type\": \"application/json\" },\n});\nexpect(created.statusCode).toBe(201);\n\n// Retrieve a service from the container\nconst userService = module.get(UserService);\n\n// Clean up\nawait module.close();\n```\n\n## Requirements\n\n- TypeScript >= 5.0\n- Bun >= 1.3.6\n- `@ambrosia-unce/core` as a peer\n- `experimentalDecorators: true` in tsconfig.json\n- `emitDecoratorMetadata: true` in tsconfig.json\n- `reflect-metadata` imported at the application entry point\n\n## License\n\nMIT\n","readmeFilename":"README.md"}