- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Laravel API Resources: Shaping JSON Responses the Right Way
About Post
return $contract; is the most tempting line in Laravel. It works, it's fast to write, and the JSON looks fine in Postman. It's also a quiet decision to make your database schema your public API, forever.
Every column you add tomorrow will appear in the response. Every column you rename will break a client. And the mobile app built against that JSON can't be updated as easily as your server can. API resources fix this by putting a small, deliberate layer between your models and the outside world. Let's build one properly, step by step.
Step 1: The basic resource
php artisan make:resource ContractResource
class ContractResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->uuid,
'reference' => $this->reference,
'status' => $this->status,
'monthly_rent' => $this->monthly_rent,
'starts_on' => $this->start_date->toDateString(),
'ends_on' => $this->end_date->toDateString(),
'created_at' => $this->created_at->toIso8601String(),
];
}
}
Use it in a controller with return new ContractResource($contract);. Inside the resource, $this proxies to the model, so $this->reference reads the model's attribute.
Already, three good things happened. The public id is a UUID, not the auto-increment key. The field names are API names (starts_on), free to differ from the columns. And dates have a fixed, documented format. Rename start_date in the database next year and only this file changes.
The one thing to never do here is return parent::toArray($request);. That returns every attribute of the model, which is exactly what you were trying to avoid.
Step 2: Relationships without N+1 queries
A contract has a tenant and a unit. The naive version is 'tenant' => new TenantResource($this->tenant). It works, and on a list of 50 contracts it quietly runs 50 extra queries.
whenLoaded() includes a relationship only if it was already eager loaded:
'tenant' => new TenantResource($this->whenLoaded('tenant')),
'unit' => new UnitResource($this->whenLoaded('unit')),
'payments_count' => $this->whenCounted('payments'),
If the controller didn't load it, the key simply isn't in the response, and no query runs. The controller decides what's included:
$contract->load(['tenant', 'unit'])->loadCount('payments');
return new ContractResource($contract);
I like this split a lot: the resource defines how things look, the controller decides what is loaded. The resource never triggers a query by surprise.
Step 3: Conditional attributes
Some fields should only appear for certain users. Finance staff see the deposit and outstanding balance; others don't.
'deposit' => $this->when(
$request->user()->can('viewFinancials', $this->resource),
$this->deposit,
),
$this->mergeWhen($request->user()->can('viewFinancials', $this->resource), [
'outstanding_balance' => $this->outstandingBalance(),
'last_payment_on' => $this->last_payment_on?->toDateString(),
]),
When the condition is false, the key disappears from the output entirely (not null, gone). $this->resource is the underlying model, which is what a policy expects. One detail: if the value is expensive to compute, pass a closure as the second argument to when(), so it only runs when the condition is true.
A useful sibling is whenNotNull(), which skips a field when its value is null, handy for optional data like a termination date.
Step 4: Collections and pagination
For lists, use collection() with a paginator:
public function index(Request $request)
{
$contracts = Contract::query()
->with('tenant')
->latest()
->paginate(20);
return ContractResource::collection($contracts);
}
Because you passed a paginator, Laravel adds links (first, last, prev, next) and meta (current page, total, per page and so on) next to the data array automatically. Clients get a consistent shape for every list endpoint without you writing any of it. With cursorPaginate(), the meta contains cursors instead of page numbers.
If you need to change that pagination block, define a paginationInformation() method on the resource or collection class. And if you need extra top-level data on any response, ->additional(['meta' => [...]]) adds it without touching the resource.
Step 5: Decide on wrapping
By default, a single resource is wrapped in a data key: {"data": {"id": ...}}. That keeps single and list responses consistent and leaves room for meta later. I'd keep it. If your API contract says otherwise, JsonResource::withoutWrapping() in a service provider's boot() method turns it off. Paginated collections keep their data key either way, because the links and meta need somewhere to sit next to it.
The rule: a model is your storage format, a resource is your API contract. Keep them separate, and you can change your database without breaking a single client.
Three gotchas worth knowing
- Accessing a relation directly in a resource (
$this->tenant->name) quietly reintroduces N+1 queries. UsewhenLoaded, or turn onModel::preventLazyLoading()in development to catch it immediately. - Null dates crash formatting.
$this->terminated_at->toDateString()throws when it's null. Use the nullsafe operator?->. - Test the shape. A feature test with
assertJsonStructure()catches the day someone removes a field a mobile app depends on.
The short version
- Never return models directly from an API.
- Choose every field, name and format on purpose.
whenLoadedandwhenCountedfor relationships,whenandmergeWhenfor permissions.collection()pluspaginate()for consistent lists.
Do you use API resources for every endpoint, or do you reach for something else, like Fractal, DTOs or plain arrays? I'm curious what's working for people.

Be first to comment it...