Http APIs
Sugoi’s #[ApiController] pattern allows you to define fully functional, OpenAPI-compliant REST APIs using simple, expressive PHP attributes.
Every route is:
- Auto-discovered
- Auto-documented with OpenAPI
- Automatically validated
- Consistently bound to typed DTOs
And it all powers live Swagger UI and OpenAPI 3.1 generation—out of the box.
Anatomy of a Controller
#[ApiController('/api/v1/users', tag: 'Users')]
class UsersController
{
#[RouteGet]
#[ResponseArray(200, 'List of users', User::class)]
public function list(): DataCollection { ... }
#[RoutePost]
#[Response(200, 'User created')]
public function create(#[BindBody] User $form): User { ... }
#[RouteGet('{id}')]
#[Response(200, 'Single user')]
public function get(#[BindPath] string $id): User { ... }
#[RouteDelete('{id}')]
#[Response(204, 'User deleted')]
public function get(#[BindPath] string $id): User { ... }
}
#[ApiController]
Defines the base path and optional OpenAPI tag for grouping.
#[RouteGet], #[RoutePost], etc.
Registers the route for its HTTP verb. Supports inline URI path segments like {id}.
#[Response], #[ResponseArray]
Documents the expected HTTP response for Swagger/OpenAPI. These do not affect runtime—they’re used strictly for API docs.
Parameter Bindings
Sugoi supports intuitive attribute-based binding for all HTTP request parts:
Attribute | Binds from |
---|---|
#[BindPath] | Path segments ({id}) |
#[BindBody] | JSON request body |
#[BindQuery] | Query string params |
#[BindHeader] | HTTP headers |
#[BindCookie] | Cookies |
Example:
public function update(
#[BindPath(type: Type::STRING_UUID)] string $id,
#[BindBody] UpdateUserRequest $payload,
#[BindHeader(name: 'Accept-Language')] $lang,
#[SensitiveParameter] // Sensitive information should be marked as such to avoid accidental logging.
#[BindHeader(name: 'Authorization')] $auth,
): void
Type Enum Support
Sugoi’s Type enum enables type narrowing beyond native PHP types:
- Type::STRING_UUID
- Type::NUMBER_DECIMAL
- Type::DATE
- Type::EPOCH
- Type::INT64
This bridges gaps between backend types and OpenAPI-compatible schemas—ensuring consistency across your frontend, backend, and documentation.
Automatic Validation
All bound types are automatically validated based on their schema:
- Missing fields, type mismatches, and malformed inputs trigger a
ValidationException
.
Sugoi does not have any opinions about how you format your error messages, all exceptions will result in a blank 500 by default. It's intended for the developer to override with their own preferred format.
Http Span
SpanHttp
is the HTTP-specific extension of the core Span class in Sugoi. It attaches contextual metadata from the incoming HttpRequest and outgoing HttpResponse, and enriches tracing output with HTTP-specific details.
A span is always created behind the scenes, but to use it you'll need to inject it in a controller function:
#[RouteGet('{id}')]
#[Response(200, 'Single user')]
public function get(SpanHttp $span, #[BindPath] string $id): User { ... }
TL;DR
- All the power of Span
- Adds HttpRequest + HttpResponse access
- Automatically injects request metadata into tracing (method, path, host, status)
Features
- getRequest() → returns the current HttpRequest
- getResponse() / setResponse() → manages the HttpResponse (if applicable)
- Enriches OpenTelemetry traces with:
- HTTP method
- URL path
- Domain and port
- Status code (when available)
This makes SpanHttp the default span used in web and API contexts, and ensures visibility of HTTP interactions in your observability tooling.
OpenAPI + Swagger UI
Sugoi generates a full OpenAPI 3.1 schema which can be outputted by running the command sugoi dump:openapi
. The dev server also exposes the spec through a bundled Swagger-UI on localhost:4040.
This includes:
- Endpoints & methods
- Request & response models
- Query/path/header parameters
- Response codes & error schemas
INFO
An OpenAPI spec will be automatically generated based on the class structures. In particular, operationIds are always generated based on the controller and method names without common suffixes, i.e "Controller".
This is enforced to support OpenAPI CodeGen out of the box.
[MethodName][ControllerName]
Example: ProjectsController@list
=> listProjects
Also note that, the Response and ResponseArray attributes are purely informational and is only used to generate response types for the generated OpenAPI spec.
Example
src/Api/Http/ProjectsController.php
#[ApiController(path: 'projects', tag: 'Projects')]
class ProjectsController
{
#[RouteGet]
#[ResponseArray(200, 'List of projects', Project::class)]
public function list(): array
{
return Project::collect(DB::table('projects')->select([
'id',
'name',
'description',
'demo_url',
'created_at',
])->get()->toArray());
}
#[RouteGet("{id}")]
#[Response(200, 'Project details', Project::class)]
public function get(#[ParamPath] int $id): Project
{
return Project::from(DB::table('projects')
->where('id', '=', $id)
->select([
'id',
'name',
'description',
'demo_url',
'created_at',
])
->firstOrFail()
);
}
#[RoutePost]
#[Response(200, 'Created project', Project::class)]
public function create(#[RequestBody] Project $body): HttpResponse
{
return HttpResponseFactory::status(501);
}
#[RoutePut("{id}")]
#[Response(200, 'Updated project details', Project::class)]
public function update(#[ParamPath] int $id, #[RequestBody] Project $body): HttpResponse
{
return HttpResponseFactory::status(501);
}
#[RouteDelete("{id}")]
#[Response(204, 'Deleted project')]
public function delete(#[ParamPath] int $id): HttpResponse
{
return HttpResponseFactory::status(501);
}
}
TIP
After setting up your first controller you can start testing the API through the bundled Swagger-UI. An API specification will be automatically generated on the fly when accessed.
Got to:
http://localhost:4040/
Configuration
HTTP configuration options.
sugoi:
http:
address: 0.0.0.0
port: 3000
path: /
Health Checks
Some features are automatically enabled for you out of the box for HTTP services. In particular SwaggerUI is bundled within the framework and auto-generates documentation based on the framework attributes and can be viewed from the dev tools.
Name | Path |
---|---|
Health Check | /healthz |