Single Page Applications (SPAs) deliver desktop-like experiences in the browser. By combining Laravel's powerful backend with Angular's dynamic frontend, you create fast, responsive applications that users love. At ZIRA Software, this stack powers our most demanding client projects.
Why Laravel + Angular?
Laravel strengths:
- RESTful API development
- Authentication & authorization
- Database management
- Background job processing
- File storage
Angular strengths:
- Two-way data binding
- Component-based architecture
- Dependency injection
- Client-side routing
- Real-time updates
Together:
- Clean separation of concerns
- Scalable architecture
- Modern user experience
- API reusable across platforms
Architecture Overview
┌─────────────────────────────────────────┐
│ Frontend (Angular) │
│ ┌──────────────────────────────────┐ │
│ │ Controllers │ Services │ │
│ │ Directives │ Factories │ │
│ │ Views │ Resources │ │
│ └──────────────────────────────────┘ │
└──────────────┬──────────────────────────┘
│ HTTP/JSON
│
┌──────────────▼──────────────────────────┐
│ Backend (Laravel) │
│ ┌──────────────────────────────────┐ │
│ │ Routes │ Controllers │ │
│ │ Models │ Services │ │
│ │ Database │ Validation │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────┘
Setting Up Laravel API
Install Laravel
composer create-project laravel/laravel blog-api
cd blog-api
Configure Database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=blog_spa
DB_USERNAME=root
DB_PASSWORD=secret
Create Post Model & Migration
php artisan make:model Post --migration
Migration:
Schema::create('posts', function (Blueprint $table) {
$table->increments('id');
$table->string('title');
$table->text('body');
$table->integer('user_id')->unsigned();
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users');
});
Model:
class Post extends Model {
protected $fillable = ['title', 'body', 'user_id'];
public function user() {
return $this->belongsTo(User::class);
}
}
RESTful API Routes
// routes/api.php
Route::group(['middleware' => 'auth:api'], function() {
Route::resource('posts', 'PostController');
});
Route::post('auth/login', 'AuthController@login');
Route::post('auth/register', 'AuthController@register');
Post Controller
<?php namespace App\Http\Controllers;
use App\Post;
use Illuminate\Http\Request;
class PostController extends Controller {
public function index() {
$posts = Post::with('user')->latest()->get();
return response()->json($posts);
}
public function store(Request $request) {
$this->validate($request, [
'title' => 'required|max:255',
'body' => 'required'
]);
$post = Post::create([
'title' => $request->title,
'body' => $request->body,
'user_id' => $request->user()->id
]);
return response()->json($post, 201);
}
public function show($id) {
$post = Post::with('user')->findOrFail($id);
return response()->json($post);
}
public function update(Request $request, $id) {
$post = Post::findOrFail($id);
$this->authorize('update', $post);
$post->update($request->only(['title', 'body']));
return response()->json($post);
}
public function destroy($id) {
$post = Post::findOrFail($id);
$this->authorize('delete', $post);
$post->delete();
return response()->json(null, 204);
}
}
CORS Middleware
Allow Angular to make cross-origin requests:
// app/Http/Middleware/Cors.php
public function handle($request, Closure $next) {
return $next($request)
->header('Access-Control-Allow-Origin', '*')
->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
->header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
}
Register middleware:
// app/Http/Kernel.php
protected $middleware = [
\App\Http\Middleware\Cors::class,
];
Setting Up Angular
Install Angular
# Install via npm
npm install -g angular
# Or via CDN in HTML
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js"></script>
Project Structure
public/
├── app/
│ ├── controllers/
│ │ ├── PostListController.js
│ │ ├── PostCreateController.js
│ │ └── PostEditController.js
│ ├── services/
│ │ ├── PostService.js
│ │ └── AuthService.js
│ ├── directives/
│ ├── filters/
│ └── app.js
├── views/
│ ├── posts/
│ │ ├── index.html
│ │ ├── create.html
│ │ └── edit.html
│ └── auth/
│ └── login.html
├── assets/
│ ├── css/
│ └── js/
└── index.html
Main Application File
// public/app/app.js
angular.module('blogApp', ['ngRoute', 'ngResource'])
.config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/posts/index.html',
controller: 'PostListController'
})
.when('/posts/create', {
templateUrl: 'views/posts/create.html',
controller: 'PostCreateController'
})
.when('/posts/:id/edit', {
templateUrl: 'views/posts/edit.html',
controller: 'PostEditController'
})
.when('/login', {
templateUrl: 'views/auth/login.html',
controller: 'AuthController'
})
.otherwise({
redirectTo: '/'
});
$locationProvider.html5Mode(true);
}])
.run(['$rootScope', '$location', 'AuthService', function($rootScope, $location, AuthService) {
$rootScope.$on('$routeChangeStart', function(event, next) {
if (next.$$route && next.$$route.auth && !AuthService.isLoggedIn()) {
event.preventDefault();
$location.path('/login');
}
});
}]);
Post Service
// public/app/services/PostService.js
angular.module('blogApp')
.factory('Post', ['$resource', function($resource) {
return $resource('http://api.blog.dev/posts/:id', {id: '@id'}, {
update: {
method: 'PUT'
}
});
}])
.factory('PostService', ['$http', function($http) {
var baseUrl = 'http://api.blog.dev';
return {
all: function() {
return $http.get(baseUrl + '/posts');
},
get: function(id) {
return $http.get(baseUrl + '/posts/' + id);
},
create: function(post) {
return $http.post(baseUrl + '/posts', post);
},
update: function(id, post) {
return $http.put(baseUrl + '/posts/' + id, post);
},
delete: function(id) {
return $http.delete(baseUrl + '/posts/' + id);
}
};
}]);
Post List Controller
// public/app/controllers/PostListController.js
angular.module('blogApp')
.controller('PostListController', ['$scope', 'PostService', function($scope, PostService) {
$scope.posts = [];
$scope.loading = true;
// Load posts
PostService.all().then(function(response) {
$scope.posts = response.data;
$scope.loading = false;
});
// Delete post
$scope.deletePost = function(id) {
if (confirm('Are you sure?')) {
PostService.delete(id).then(function() {
$scope.posts = $scope.posts.filter(function(post) {
return post.id !== id;
});
});
}
};
}]);
Post List View
<!-- views/posts/index.html -->
<div class="container">
<h1>Blog Posts</h1>
<a href="/posts/create" class="btn btn-primary">Create New Post</a>
<div ng-show="loading">Loading...</div>
<div ng-hide="loading">
<div class="post" ng-repeat="post in posts">
<h2>{{ post.title }}</h2>
<p>{{ post.body }}</p>
<p class="meta">
By {{ post.user.name }} on {{ post.created_at | date }}
</p>
<a href="/posts/{{ post.id }}/edit">Edit</a>
<button ng-click="deletePost(post.id)">Delete</button>
</div>
</div>
</div>
Post Create Controller
// public/app/controllers/PostCreateController.js
angular.module('blogApp')
.controller('PostCreateController', ['$scope', '$location', 'PostService',
function($scope, $location, PostService) {
$scope.post = {};
$scope.errors = {};
$scope.submitPost = function() {
PostService.create($scope.post)
.then(function(response) {
$location.path('/');
})
.catch(function(response) {
if (response.status === 422) {
$scope.errors = response.data;
}
});
};
}]);
Post Create View
<!-- views/posts/create.html -->
<div class="container">
<h1>Create New Post</h1>
<form ng-submit="submitPost()">
<div class="form-group" ng-class="{'has-error': errors.title}">
<label>Title</label>
<input type="text" ng-model="post.title" class="form-control">
<span class="help-block" ng-show="errors.title">
{{ errors.title[0] }}
</span>
</div>
<div class="form-group" ng-class="{'has-error': errors.body}">
<label>Body</label>
<textarea ng-model="post.body" class="form-control" rows="10"></textarea>
<span class="help-block" ng-show="errors.body">
{{ errors.body[0] }}
</span>
</div>
<button type="submit" class="btn btn-primary">Create Post</button>
<a href="/" class="btn btn-default">Cancel</a>
</form>
</div>
Authentication with JWT
Install JWT Package
composer require tymon/jwt-auth
Configure JWT
// config/app.php
'providers' => [
Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class,
],
'aliases' => [
'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class,
],
Generate secret:
php artisan jwt:generate
Auth Controller
class AuthController extends Controller {
public function login(Request $request) {
$credentials = $request->only('email', 'password');
if (!$token = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'Invalid credentials'], 401);
}
return response()->json(compact('token'));
}
public function register(Request $request) {
$this->validate($request, [
'name' => 'required',
'email' => 'required|email|unique:users',
'password' => 'required|min:6'
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => bcrypt($request->password)
]);
$token = JWTAuth::fromUser($user);
return response()->json(compact('token'), 201);
}
}
Angular Auth Service
angular.module('blogApp')
.factory('AuthService', ['$http', '$window', function($http, $window) {
var baseUrl = 'http://api.blog.dev';
return {
login: function(credentials) {
return $http.post(baseUrl + '/auth/login', credentials)
.then(function(response) {
$window.localStorage.setItem('token', response.data.token);
return response.data;
});
},
logout: function() {
$window.localStorage.removeItem('token');
},
isLoggedIn: function() {
return !!$window.localStorage.getItem('token');
},
getToken: function() {
return $window.localStorage.getItem('token');
}
};
}]);
HTTP Interceptor
Add auth token to all requests:
angular.module('blogApp')
.factory('AuthInterceptor', ['$window', function($window) {
return {
request: function(config) {
var token = $window.localStorage.getItem('token');
if (token) {
config.headers.Authorization = 'Bearer ' + token;
}
return config;
},
responseError: function(response) {
if (response.status === 401) {
$window.location = '/login';
}
return response;
}
};
}])
.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('AuthInterceptor');
}]);
Real-Time Updates with Pusher
Install Pusher
composer require pusher/pusher-php-server
Configure Broadcasting
// .env
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your-app-id
PUSHER_KEY=your-key
PUSHER_SECRET=your-secret
Broadcast Events
// app/Events/PostCreated.php
class PostCreated implements ShouldBroadcast {
use SerializesModels;
public $post;
public function __construct(Post $post) {
$this->post = $post;
}
public function broadcastOn() {
return ['posts-channel'];
}
}
// Fire event
event(new PostCreated($post));
Listen in Angular
// Include Pusher library
<script src="https://js.pusher.com/3.0/pusher.min.js"></script>
// In controller
var pusher = new Pusher('your-key');
var channel = pusher.subscribe('posts-channel');
channel.bind('App\\Events\\PostCreated', function(data) {
$scope.$apply(function() {
$scope.posts.unshift(data.post);
});
});
Best Practices
- API Versioning
Route::group(['prefix' => 'api/v1'], function() {
// routes
});
- Response Transformers
return fractal()
->collection($posts)
->transformWith(new PostTransformer())
->toArray();
- Rate Limiting
Route::group(['middleware' => 'throttle:60,1'], function() {
// 60 requests per minute
});
- Error Handling
$http.get('/api/posts')
.then(success)
.catch(error)
.finally(cleanup);
- Loading States
$scope.loading = true;
PostService.all().finally(function() {
$scope.loading = false;
});
Conclusion
Laravel and Angular create powerful, modern web applications. Laravel handles backend concerns brilliantly while Angular delivers exceptional user experiences. At ZIRA Software, this combination has proven itself across dozens of production applications.
The architecture scales from simple blogs to complex enterprise systems, making it an excellent choice for ambitious projects.
Ready to build a modern SPA? Contact ZIRA Software to discuss Laravel + Angular development, API design, and scalable application architecture.