Utilizing Claude Skills in client projects
While working on a project for Ticketmatic, we repeatedly encountered the same pattern. We use the Saloon library to integrate with the Ticketmatic API, and our application relies on it heavily, essentially functioning like an SPA where almost every interaction flows through their API. If you're familiar with Saloon, you know what this means: a dedicated request class for every endpoint, and a DTO to parse both the response data and the request body. Multiply that across dozens of endpoints and you end up with a lot of repetitive work.
There are Artisan console commands that try to help with this, such as php artisan saloon:response, but even then we still have to write our own DTOs, add parameters manually, import everything, and wire it all together. A lot of the work is still repetitive and manual.
Where we came from: doing the same work, just faster
Every new endpoint followed the same four steps. Here's what it looks like to add a simple "get order" call:
1. Create a Params class
A small DTO that holds the request parameters and maps them to the API's expected keys:
class GetOrderParams
{
public function __construct(
public int $orderId,
//only one property for simplicity
) {}
public function toTicketmaticParams(): array
{
return ['orderId' => $this->orderId];
}
}
2. Create a Resource DTO
A spatie/laravel-data object with a factory method that maps the API's lowercase keys to typed camelCase properties:
class Order extends Data
{
public function __construct(
public int $orderId,
public string $status,
// ...
) {}
public static function fromTicketmatic(array $data): self
{
return new self(
orderId: $data['orderid'],
status: $data['status'],
);
}
}
3. Create the Request class
The Saloon request that ties everything together, endpoint, HTTP method, session handling, and DTO transformation:
class GetOrderRequest extends Request
{
use WithSession;
protected Method $method = Method::GET;
public function __construct(
protected GetOrderParams $params,
) {}
public function resolveEndpoint(): string
{
return "user/orders/{$this->params->orderId}";
}
public function createDtoFromResponse(Response $response): Order
{
return Order::fromTicketmatic($response->json());
}
}
A few conventions to keep in mind: GET requests extend Request directly, POST/PUT requests extend TicketShopRequest (which adds HasJsonBody), and most requests use the WithSession trait to capture session headers.
4. Add a method on the Resource
public function get(GetOrderParams $params): ?Order
{
return $this->connector->send(new GetOrderRequest($params))
->dtoOrFail();
}
Which gives us a clean call chain:
app(TicketShop::class)->orders()->get(new GetOrderParams(123));
None of this is hard. It's just repeating the same pattern over and over again.
Why did we do this manually?
In the beginning, I was going into the right folders and creating these files myself, copying existing Requests and adjusting namespaces, methods, and so on. Around December 2025, Claude really started to improve and actually do what it was told. I began asking Claude to generate the request files for me, and then also the resource files. That already made creating new Saloon requests much faster. Later, something called "Claude Skills" was introduced, which was very interesting. Skills are reusable instruction sets for AI coding assistants that activate automatically based on context.
As you can see in the video, Claude asks a series of questions about the request we want to create. Once it has all the necessary information, it generates the files in the correct directories, fixes namespaces, adds the properties we defined, handles required and optional fields, and even creates the DTOs for us. We also specified in the skill to run the Laravel Pint formatter.
Imagine having to manually add 15 properties to a request body DTO and then another 15 to the response DTO — that's tedious, repetitive work. Now, we can simply copy the API specifications and feed them directly to the skill, and it handles all of that for us.
In about 9 out of 10 cases, everything is correct: files, properties, and namespaces. That said, it's still important to review the generated code. There's always that remaining 10% where something isn't quite right, usually because we didn't provide enough context.
Other skills
The Saloon skill is not the only one we wrote for this project. Another repetitive task was creating new pages. Yes, Livewire has Artisan commands to help scaffold components, but in this codebase we do some extra work on top of that. For example, we have our own Shop config class that registers Livewire components in an array:
class Shop
{
// Alias => Component Class
protected array $components = [
// Global
ShopComponent::GlobalFlash->value => Flash::class,
ShopComponent::GlobalOptIns->value => OptIns::class,
ShopComponent::GlobalAddressFields->value => AddressFields::class,
ShopComponent::GlobalLanguagePicker->value => LanguagePicker::class,
// Auth
ShopComponent::AuthLogIn->value => LogIn::class,
ShopComponent::AuthForgotPassword->value => ForgotPassword::class,
ShopComponent::AuthResetPassword->value => ResetPassword::class,
// Event
ShopComponent::EventIndex->value => EventIndex::class,
ShopComponent::EventSidebar->value => SeatingSidebar::class,
ShopComponent::EventRanks->value => SeatingRanks::class,
//...
];
Each component is mapped to an enum value that serves as its alias:
enum ShopComponent: string
{
// Global
case GlobalFlash = 'tm.global.flash';
//...
}
If you've worked with Livewire, you probably know that it registers components automatically — and yes, that's true. We wrote this Shop config class later in the development phase for a specific reason: this project is designed as a package that other Laravel apps will be built on top of. Some apps might need to disable certain features or swap in custom implementations, and this registry gives us that flexibility.
So every time we add a new Livewire component, it's not just creating the component itself. We also have to add a case to the enum, register it in the Shop config class, and include the custom traits we commonly use. Another perfect candidate for a skill. Here is a video demonstrating how we would go about making a login page:
As you can see, it works really well. We get our new Livewire component, our Blade template (everything already in the correct directories!), the component comes with the methods and traits we commonly use, and everything is registered via the Shop class.
Beyond scaffolding components, these skills also help enforce consistency across the codebase. For example, I built a review-pr skill that reviews my pull requests before they ever reach my colleague Seb. It catches common oversights like untranslated strings or inconsistent naming and helps keep our conventions intact.
Taken together, these small automations remove friction from everyday work. Less repetition, fewer mistakes, and more time spent on the parts that actually matter.