Laravel Lumen is Laravel's micro-framework sibling, built for microservices and APIs where speed matters most. By removing unnecessary features and optimizing for performance, Lumen delivers responses in milliseconds. At ZIRA Software, we use Lumen for high-throughput services processing millions of requests daily.
Why Lumen?
Performance advantages:
- Up to 2x faster than full Laravel
- Minimal bootstrapping overhead
- Optimized routing
- No session/view overhead
- Stateless by default
- Perfect for microservices
When to use Lumen:
- RESTful APIs
- Microservices architecture
- High-traffic services
- Stateless applications
- Service-oriented architecture
- Internal APIs
When NOT to use Lumen:
- Applications needing sessions
- Traditional web applications
- When you need Blade templates extensively
- Rapid prototyping (use full Laravel)
Installation
composer create-project --prefer-dist laravel/lumen blog-api
cd blog-api
Directory structure:
blog-api/
├── app/
│ ├── Http/
│ │ ├── Controllers/
│ │ ├── Middleware/
│ │ └── routes.php
│ ├── Providers/
│ └── User.php
├── bootstrap/
├── database/
├── public/
├── resources/
├── storage/
├── tests/
└── .env
Leaner than full Laravel - only what you need.
Configuration
.env:
APP_ENV=local
APP_DEBUG=true
APP_KEY=your-32-char-random-string
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=blog_api
DB_USERNAME=root
DB_PASSWORD=
CACHE_DRIVER=redis
QUEUE_DRIVER=redis
bootstrap/app.php:
<?php
require_once __DIR__.'/../vendor/autoload.php';
try {
(new Dotenv\Dotenv(__DIR__.'/../'))->load();
} catch (Dotenv\Exception\InvalidPathException $e) {
//
}
$app = new Laravel\Lumen\Application(
realpath(__DIR__.'/../')
);
// Enable Eloquent and Facades (optional)
$app->withEloquent();
$app->withFacades();
// Register middleware
$app->middleware([
// App\Http\Middleware\CorsMiddleware::class
]);
$app->routeMiddleware([
'auth' => App\Http\Middleware\Authenticate::class,
]);
// Register service providers
$app->register(App\Providers\AppServiceProvider::class);
// $app->register(App\Providers\AuthServiceProvider::class);
// Load routes
$app->group(['namespace' => 'App\Http\Controllers'], function ($app) {
require __DIR__.'/../app/Http/routes.php';
});
return $app;
Routing
app/Http/routes.php:
<?php
$app->get('/', function () use ($app) {
return $app->version();
});
// Basic routes
$app->get('/posts', 'PostController@index');
$app->get('/posts/{id}', 'PostController@show');
$app->post('/posts', 'PostController@store');
$app->put('/posts/{id}', 'PostController@update');
$app->delete('/posts/{id}', 'PostController@destroy');
// Route groups
$app->group(['prefix' => 'api/v1'], function () use ($app) {
// Public routes
$app->get('/posts', 'PostController@index');
$app->get('/posts/{id}', 'PostController@show');
// Protected routes
$app->group(['middleware' => 'auth'], function () use ($app) {
$app->post('/posts', 'PostController@store');
$app->put('/posts/{id}', 'PostController@update');
$app->delete('/posts/{id}', 'PostController@destroy');
});
});
// Named routes
$app->get('/profile', ['as' => 'profile', 'uses' => 'UserController@profile']);
Controllers
app/Http/Controllers/PostController.php:
<?php namespace App\Http\Controllers;
use App\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
/**
* List all posts
*/
public function index(Request $request)
{
$perPage = $request->input('per_page', 15);
$posts = Post::with('author')
->published()
->latest()
->paginate($perPage);
return response()->json($posts);
}
/**
* Show single post
*/
public function show($id)
{
$post = Post::with('author', 'comments')
->findOrFail($id);
return response()->json($post);
}
/**
* Create post
*/
public function store(Request $request)
{
$this->validate($request, [
'title' => 'required|max:255',
'content' => 'required',
'author_id' => 'required|exists:users,id'
]);
$post = Post::create($request->all());
return response()->json($post, 201);
}
/**
* Update post
*/
public function update(Request $request, $id)
{
$post = Post::findOrFail($id);
$this->validate($request, [
'title' => 'max:255',
]);
$post->update($request->all());
return response()->json($post);
}
/**
* Delete post
*/
public function destroy($id)
{
$post = Post::findOrFail($id);
$post->delete();
return response()->json(null, 204);
}
}
Models
app/Post.php:
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $fillable = ['title', 'slug', 'content', 'author_id', 'published_at'];
protected $hidden = ['created_at', 'updated_at'];
protected $casts = [
'published_at' => 'datetime',
];
/**
* Relationships
*/
public function author()
{
return $this->belongsTo(User::class, 'author_id');
}
public function comments()
{
return $this->hasMany(Comment::class);
}
/**
* Scopes
*/
public function scopePublished($query)
{
return $query->whereNotNull('published_at')
->where('published_at', '<=', now());
}
/**
* Accessors
*/
public function getExcerptAttribute()
{
return substr($this->content, 0, 200) . '...';
}
}
Middleware
app/Http/Middleware/CorsMiddleware.php:
<?php namespace App\Http\Middleware;
use Closure;
class CorsMiddleware
{
/**
* Handle CORS
*/
public function handle($request, Closure $next)
{
$headers = [
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'POST, GET, OPTIONS, PUT, DELETE',
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Max-Age' => '86400',
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-Requested-With'
];
if ($request->isMethod('OPTIONS')) {
return response()->json('{"method":"OPTIONS"}', 200, $headers);
}
$response = $next($request);
foreach ($headers as $key => $value) {
$response->header($key, $value);
}
return $response;
}
}
Rate limiting:
<?php namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Cache;
class RateLimitMiddleware
{
public function handle($request, Closure $next, $maxAttempts = 60)
{
$key = $this->resolveRequestSignature($request);
if (Cache::has($key) && Cache::get($key) >= $maxAttempts) {
return response()->json([
'error' => 'Too many requests'
], 429);
}
Cache::add($key, 0, 1);
Cache::increment($key);
return $next($request);
}
protected function resolveRequestSignature($request)
{
return sha1($request->ip() . '|' . $request->path());
}
}
Authentication
JWT Authentication
Install JWT:
composer require tymon/jwt-auth:1.0.0-beta.3
Register provider in bootstrap/app.php:
$app->register(Tymon\JWTAuth\Providers\LumenServiceProvider::class);
AuthController:
<?php namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Tymon\JWTAuth\JWTAuth;
class AuthController extends Controller
{
protected $jwt;
public function __construct(JWTAuth $jwt)
{
$this->jwt = $jwt;
}
/**
* Login
*/
public function login(Request $request)
{
$this->validate($request, [
'email' => 'required|email',
'password' => 'required'
]);
$credentials = $request->only(['email', 'password']);
if (!$token = $this->jwt->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return response()->json([
'token' => $token,
'token_type' => 'bearer',
'expires_in' => $this->jwt->factory()->getTTL() * 60
]);
}
/**
* Get authenticated user
*/
public function me()
{
return response()->json($this->jwt->user());
}
/**
* Logout
*/
public function logout()
{
$this->jwt->invalidate($this->jwt->getToken());
return response()->json(['message' => 'Successfully logged out']);
}
/**
* Refresh token
*/
public function refresh()
{
return response()->json([
'token' => $this->jwt->refresh()
]);
}
}
Protected routes:
$app->group(['middleware' => 'auth:api'], function () use ($app) {
$app->get('/me', 'AuthController@me');
$app->post('/posts', 'PostController@store');
});
Database
Migration:
php artisan make:migration create_posts_table
database/migrations/xxxx_create_posts_table.php:
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreatePostsTable extends Migration
{
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->increments('id');
$table->string('title');
$table->string('slug')->unique();
$table->text('content');
$table->integer('author_id')->unsigned();
$table->timestamp('published_at')->nullable();
$table->timestamps();
$table->foreign('author_id')
->references('id')
->on('users')
->onDelete('cascade');
$table->index('published_at');
});
}
public function down()
{
Schema::dropIfExists('posts');
}
}
Run migrations:
php artisan migrate
Seeders:
<?php
use Illuminate\Database\Seeder;
class PostsTableSeeder extends Seeder
{
public function run()
{
factory(App\Post::class, 50)->create();
}
}
Model factories:
<?php
$factory->define(App\Post::class, function (Faker\Generator $faker) {
return [
'title' => $faker->sentence,
'slug' => $faker->slug,
'content' => $faker->paragraphs(5, true),
'author_id' => factory(App\User::class)->create()->id,
'published_at' => $faker->dateTimeBetween('-1 year', 'now'),
];
});
Validation
Custom validation:
<?php namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class PostController extends Controller
{
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'title' => 'required|max:255|unique:posts',
'content' => 'required|min:100',
'author_id' => 'required|exists:users,id',
'tags' => 'array',
'tags.*' => 'string|max:50'
], [
'title.required' => 'Post title is required',
'content.min' => 'Content must be at least 100 characters'
]);
if ($validator->fails()) {
return response()->json([
'errors' => $validator->errors()
], 422);
}
$post = Post::create($request->all());
return response()->json($post, 201);
}
}
Error Handling
app/Exceptions/Handler.php:
<?php namespace App\Exceptions;
use Exception;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Laravel\Lumen\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpException;
class Handler extends ExceptionHandler
{
protected $dontReport = [
AuthorizationException::class,
HttpException::class,
ModelNotFoundException::class,
ValidationException::class,
];
public function render($request, Exception $e)
{
// Model not found
if ($e instanceof ModelNotFoundException) {
return response()->json([
'error' => 'Resource not found'
], 404);
}
// Validation failed
if ($e instanceof ValidationException) {
return response()->json([
'errors' => $e->validator->errors()
], 422);
}
// Unauthorized
if ($e instanceof AuthorizationException) {
return response()->json([
'error' => 'Unauthorized'
], 403);
}
// HTTP exceptions
if ($e instanceof HttpException) {
return response()->json([
'error' => $e->getMessage()
], $e->getStatusCode());
}
// Default error response
if (env('APP_DEBUG')) {
return parent::render($request, $e);
}
return response()->json([
'error' => 'Internal server error'
], 500);
}
}
Caching
<?php namespace App\Http\Controllers;
use App\Post;
use Illuminate\Support\Facades\Cache;
class PostController extends Controller
{
public function index()
{
$posts = Cache::remember('posts.all', 60, function () {
return Post::with('author')
->published()
->latest()
->get();
});
return response()->json($posts);
}
public function show($id)
{
$post = Cache::remember("posts.{$id}", 60, function () use ($id) {
return Post::with('author', 'comments')
->findOrFail($id);
});
return response()->json($post);
}
public function store(Request $request)
{
$post = Post::create($request->all());
// Clear cache
Cache::forget('posts.all');
return response()->json($post, 201);
}
}
Queue Jobs
Create job:
php artisan make:job ProcessPodcast
app/Jobs/ProcessPodcast.php:
<?php namespace App\Jobs;
class ProcessPodcast extends Job
{
protected $podcast;
public function __construct($podcast)
{
$this->podcast = $podcast;
}
public function handle()
{
// Process podcast
sleep(5);
$this->podcast->update(['processed' => true]);
}
}
Dispatch job:
dispatch(new ProcessPodcast($podcast));
Run queue worker:
php artisan queue:work
Testing
tests/PostTest.php:
<?php
class PostTest extends TestCase
{
/** @test */
public function it_lists_posts()
{
factory(App\Post::class, 5)->create();
$this->get('/api/v1/posts');
$this->seeStatusCode(200);
$this->seeJsonStructure([
'*' => ['id', 'title', 'content', 'author']
]);
}
/** @test */
public function it_creates_post()
{
$data = [
'title' => 'Test Post',
'content' => 'Test content',
'author_id' => factory(App\User::class)->create()->id
];
$this->post('/api/v1/posts', $data);
$this->seeStatusCode(201);
$this->seeJson(['title' => 'Test Post']);
$this->seeInDatabase('posts', ['title' => 'Test Post']);
}
/** @test */
public function it_validates_required_fields()
{
$this->post('/api/v1/posts', []);
$this->seeStatusCode(422);
$this->seeJson(['title' => ['The title field is required.']]);
}
/** @test */
public function it_updates_post()
{
$post = factory(App\Post::class)->create();
$this->put("/api/v1/posts/{$post->id}", [
'title' => 'Updated Title'
]);
$this->seeStatusCode(200);
$this->seeInDatabase('posts', [
'id' => $post->id,
'title' => 'Updated Title'
]);
}
/** @test */
public function it_deletes_post()
{
$post = factory(App\Post::class)->create();
$this->delete("/api/v1/posts/{$post->id}");
$this->seeStatusCode(204);
$this->notSeeInDatabase('posts', ['id' => $post->id]);
}
}
Run tests:
vendor/bin/phpunit
API Versioning
<?php
// Version 1
$app->group(['prefix' => 'api/v1', 'namespace' => 'App\Http\Controllers\V1'], function () use ($app) {
$app->get('/posts', 'PostController@index');
});
// Version 2
$app->group(['prefix' => 'api/v2', 'namespace' => 'App\Http\Controllers\V2'], function () use ($app) {
$app->get('/posts', 'PostController@index');
});
API Documentation
Use API Blueprint or Swagger:
# api-docs.yml
swagger: '2.0'
info:
title: Blog API
version: '1.0'
paths:
/posts:
get:
summary: List posts
responses:
200:
description: Success
schema:
type: array
items:
$ref: '#/definitions/Post'
/posts/{id}:
get:
summary: Get post
parameters:
- name: id
in: path
required: true
type: integer
responses:
200:
description: Success
schema:
$ref: '#/definitions/Post'
404:
description: Not found
definitions:
Post:
type: object
properties:
id:
type: integer
title:
type: string
content:
type: string
Performance Optimization
1. Optimize autoloader:
composer dump-autoload --optimize
2. Cache config:
// Don't use config() in Lumen - it's slow
// Instead, use env() directly or cache values
3. Use Redis for cache and sessions:
composer require predis/predis
4. Database query optimization:
// Eager load relationships
Post::with('author', 'comments')->get();
// Select specific columns
Post::select('id', 'title', 'published_at')->get();
// Use chunk for large datasets
Post::chunk(100, function ($posts) {
foreach ($posts as $post) {
// Process
}
});
5. Response caching:
return Cache::remember('posts.index', 60, function () {
return Post::all();
});
Deployment
Optimize for production:
composer install --no-dev --optimize-autoloader
Nginx configuration:
server {
listen 80;
server_name api.example.com;
root /var/www/blog-api/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
}
Monitoring
Log requests:
<?php namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Log;
class LogRequests
{
public function handle($request, Closure $next)
{
$start = microtime(true);
$response = $next($request);
$duration = microtime(true) - $start;
Log::info('API Request', [
'method' => $request->method(),
'path' => $request->path(),
'status' => $response->status(),
'duration' => round($duration * 1000, 2) . 'ms',
'ip' => $request->ip()
]);
return $response;
}
}
Conclusion
Laravel Lumen excels at building fast, focused microservices and APIs. While it sacrifices some Laravel features for speed, the performance gains are substantial. At ZIRA Software, Lumen powers our highest-traffic services, handling millions of requests with minimal resources.
Start with Lumen for stateless APIs and microservices. If you need more Laravel features later, upgrading is straightforward.
Building high-performance APIs or microservices? Contact ZIRA Software to discuss how we can architect and build scalable services with Laravel Lumen.