Friday, 3 August, 2018 UTC


Summary

When building modern applications, it is not uncommon to have several isolated yet connected aspects of your application. For instance, you can have a website, an admin panel, and an API all powering the same application. There are many ways you can decide to handle this setup and we will consider one of the ways you can do this in this article.
We will be building an application with an administrative dashboard, the main web app, and an API that serves both the admin and the web app. We will be using just one Laravel codebase to do this and we will use Laravel’s subdomain routing to make sure the request are routed correctly. Let’s get started.
Prerequisites
To follow along you will need the following:
  • Basic knowledge of the Laravel framework.
  • Laravel CLI installed on your machine.
  • Valet installed on your machine.
  • A code editor like Visual Studio Code.
  • SQLite installed on your machine.
Valet is only officially available to Mac users. However, there are ports for both Linux and Windows available.
If you have all the requirements, let’s start.
Setting up your environment
The first thing you need to do is to create a new Laravel project. This will be the base for our application. Create a new Laravel project using the command below:
    $ laravel new acme
This will create a new Laravel application in an acme directory. Open the project in a code editor of your choice.
The next thing we need to do is create our primary domain using Valet and then add the other two proposed subdomains. We will be using the valet link command to create the local domains. Read more about serving sites using Valet.
In the root of your application run the following commands:
    $ valet link acme
    $ valet link api.acme
    $ valet link admin.acme
At this point, if you visit either of the URL’s below, you should be pointed to the same Laravel welcome page:
  • http://acme.test
  • http://api.acme.test
  • http://admin.acme.test
We will be using the Laravel subdomain routing to route different parts of the application to different logic.
Open the app/Providers/RouteServiceProvider.php file and replace the contents map method with the following:
    public function map()
    {
        $this->mapApiRoutes();

        $this->mapAdminRoutes();

        $this->mapWebRoutes();
    }
Next, replace the mapApiRoutes and mapWebRoutes in the same class with the following:
    protected function mapApiRoutes()
    {
        Route::domain('api.acme.test')
             ->middleware('api')
             ->namespace($this->namespace)
             ->group(base_path('routes/api.php'));
    }

    protected function mapWebRoutes()
    {
        Route::domain('acme.test')
             ->middleware('web')
             ->namespace($this->namespace)
             ->group(base_path('routes/web.php'));
    }
In the methods above, we have added the domain method, which will tell Laravel to respond to the request if the domain matches what we passed as the parameter.
Now, add the new mapAdminRoutes method below to the class:
    protected function mapAdminRoutes()
    {
        Route::domain('admin.acme.test')
             ->middleware('web')
             ->namespace($this->namespace)
             ->group(base_path('routes/admin.php'));
    }
As seen from the three methods above, we pass the path to the file where the routes are defined. However, the mapAdminRoutes references a file that does not yet exist so let’s create it. Create a new admin.php file in the routes directory and add the following:
    <?php

    Route::get('/', function () {
        return 'Admin!';
    });
Open the routes/api.php file and replace the contents with the following:
    <?php

    Route::get('/', function () {
        return ['hello'];
    });
Now when you visit each of the three domains, they should show something different. Great. Let’s build something in the domains to see how they can work together.
Building a simple API for the blog
The first thing we want to build is an API to power our blog. To keep it simple, we will add three endpoints. The first will be to show all the posts available, the second to show a single post, and the final one will be to create a post.

Setting up a database connection

The first thing we want to set up is a database connection. We will be using SQLite so the connection is easier. Create a database.sqlite file in your projects database directory. Open your .env file and add replace the following keys:
    DB_CONNECTION=mysql
    DB_HOST=127.0.0.1
    DB_PORT=3306
    DB_DATABASE=homestead
    DB_USERNAME=homestead
    DB_PASSWORD=secret
With:
    DB_CONNECTION=sqlite
    DB_DATABASE=/full/path/to/database.sqlite

Setting up migration and seed data

Next, let’s create a migration, model, and controller for the Post resource. Run the command below to do this:
    $ php artisan make:model Post -mr
Tip: The -mr flag stands for migration and resource controller. This means that in addition to creating a model, the command will also create a migration and resource controller.
Open the Post class in the app directory and replace the contents with the following code:
    <?php
    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Post extends Model
    {
        protected $fillable = ['title', 'content'];
    }
Open the *_create_posts_table.php file that was created in the database/migrations directory and replace the up method with the following:
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->text('content');
            $table->timestamps();
        });
    }
Next, let’s create a model factory. Model factories make it easy for us to seed large test data for testing. Run the command below to create a model factory for our Post resource:
    $ php artisan make:factory PostFactory
Open the PostFactory file in the database/factories directory and replace the contents with the following:
    <?php
    use App\Post;
    use Faker\Generator as Faker;

    $factory->define(Post::class, function (Faker $faker) {
        return [
            'title' => $faker->sentence(),
            'content' => $faker->paragraphs(10, true)
        ];
    });
Now create a database seeder using the command below:
    $ php artisan make:seeder PostsTableSeeder
Open the PostsTableSeeder class in the database/seeds directory and replace the run method with the following:
    public function run()
    {
        factory(\App\Post::class, 10)->create();
    }
In the code above, we instruct the model factory to generate 10 sample posts whenever the seeder is run.
Next, open the DatabaseSeeder file in the database/seeds directory and replace the run class with the following:
    public function run()
    {
        $this->call(PostsTableSeeder::class);
    }
Now let’s run our migration and seed some sample data into the database:
    $ php artisan migrate --seed 
Let’s create our endpoints.

Creating our endpoints

Open the PostController and replace the index, store, and show methods with the following:
    public function index(Post $post)
    {
        return response()->json($post->paginate()->toArray());
    }

    public function store(Request $request, Post $post)
    {
        $data = $request->validate([
            'title' => 'required|string|between:1,50',
            'content' => 'required|string|between:10,5000',
        ]);

        return response()->json($post->create($data)->toArray());
    }

    public function show(Post $post)
    {
        return response()->json($post->toArray());
    }
Then open the routes/api.php file and replace the contents with the following:
    <?php

    Route::get('/posts/{post}', '[email protected]');
    Route::get('/posts', '[email protected]');
    Route::post('/posts', '[email protected]');
At this point, we have created the API endpoints to show all posts, show one post, and create a post.
If you visit the API endpoint, http://api.acme.test/posts you should see all the posts displayed:
Building a simple web app
Now that we have a working API, let’s build a web app that will consume the data. The web app will simply display all the posts available using the API.
Open the routes/web.php file and replace the contents with the following:
    <?php

    Route::view('/', 'web.index');
Next, create a web directory in resources/views, and in there create an index.blade.php file. Inside the new file paste the following:
    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
      <title>Blog Template for Bootstrap</title>
      <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet">
      <link href="https://fonts.googleapis.com/css?family=Playfair+Display:700,900" rel="stylesheet">
      <link href="{{ asset('css/blog.css') }}" rel="stylesheet">
    </head>
    <body>
      <div class="container">
        <header class="blog-header py-3">
          <div class="row flex-nowrap justify-content-between align-items-center">
            <div class="col-4 pt-1">
              <a class="text-muted" href="#">Subscribe</a>
            </div>
            <div class="col-4 text-center">
              <a class="blog-header-logo text-dark" href="#">Large</a>
            </div>
            <div class="col-4 d-flex justify-content-end align-items-center">
              <a class="text-muted" href="#">
                <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mx-3"><circle cx="10.5" cy="10.5" r="7.5"></circle><line x1="21" y1="21" x2="15.8" y2="15.8"></line></svg>
              </a>
              <a class="btn btn-sm btn-outline-secondary" href="#">Sign up</a>
            </div>
          </div>
        </header>
        <div class="nav-scroller py-1 mb-2">
          <nav class="nav d-flex justify-content-between">
            <a class="p-2 text-muted" href="#">World</a>
            <a class="p-2 text-muted" href="#">U.S.</a>
            <a class="p-2 text-muted" href="#">Technology</a>
            <a class="p-2 text-muted" href="#">Design</a>
            <a class="p-2 text-muted" href="#">Culture</a>
            <a class="p-2 text-muted" href="#">Business</a>
            <a class="p-2 text-muted" href="#">Politics</a>
            <a class="p-2 text-muted" href="#">Opinion</a>
            <a class="p-2 text-muted" href="#">Science</a>
            <a class="p-2 text-muted" href="#">Health</a>
            <a class="p-2 text-muted" href="#">Style</a>
            <a class="p-2 text-muted" href="#">Travel</a>
          </nav>
        </div>
          <div class="jumbotron p-3 p-md-5 text-white rounded bg-dark">
              <div class="col-md-6 px-0">
              <h1 class="display-4 font-italic">Title of a longer featured blog post</h1>
              <p class="lead my-3">Multiple lines of text that form the lede, informing new readers quickly and efficiently about what's most interesting in this post's contents.</p>
              <p class="lead mb-0"><a href="#" class="text-white font-weight-bold">Continue reading...</a></p>
              </div>
          </div>
      </div>

      <main role="main" class="container" id="app">
        <div class="row">
          <div class="col-md-8 blog-main">
            <h3 class="pb-3 mb-4 font-italic border-bottom">
              From the Firehose
            </h3>
            <div class="blog-post" v-for="post in posts">
              <h2 class="blog-post-title">@{{ post.title }}</h2>
              <p class="blog-post-meta">January 1, 2014 by <a href="#">Neo</a></p>
              <p>@{{ post.content }}</p>
            </div>
          </div>
          <aside class="col-md-4 blog-sidebar">
            <div class="p-3 mb-3 bg-light rounded">
              <h4 class="font-italic">About</h4>
              <p class="mb-0">Etiam porta <em>sem malesuada magna</em> mollis euismod. Cras mattis consectetur purus sit amet fermentum. Aenean lacinia bibendum nulla sed consectetur.</p>
            </div>
            <div class="p-3">
              <h4 class="font-italic">Archives</h4>
              <ol class="list-unstyled mb-0">
                <li><a href="#">March 2018</a></li>
                <li><a href="#">February 2018</a></li>
              </ol>
            </div>
          </aside>
        </div>
      </main>
      <footer class="blog-footer">
        <p>Blog template built for <a href="https://getbootstrap.com/">Bootstrap</a> by <a href="https://twitter.com/mdo">@mdo</a>.</p>
        <p>
          <a href="#">Back to top</a>
        </p>
      </footer>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
      <script>
      new Vue({
          el: '#app',
          data: {
              posts: []
          },
          created() {
              axios.get('http://api.acme.test/posts').then(res => this.posts = res.data.data)
          }
      })
      </script>
    </body>
    </html>
In the HTML above, we fetched the posts using Axios and displayed them using Vue. We referenced a blog.css file, so let’s create that. In the public/css directory, create a new blog.css file and paste in the following:
    .blog-header {
      line-height: 1;
      border-bottom: 1px solid #e5e5e5;
    }
    .blog-header-logo {
      font-family: "Playfair Display", Georgia, "Times New Roman", serif;
      font-size: 2.25rem;
    }
    .blog-header-logo:hover {
      text-decoration: none;
    }
    h1, h2, h3, h4, h5, h6 {
      font-family: "Playfair Display", Georgia, "Times New Roman", serif;
    }
    .display-4 {
      font-size: 2.5rem;
    }
    @media (min-width: 768px) {
      .display-4 {
        font-size: 3rem;
      }
    }
    .nav-scroller {
      position: relative;
      z-index: 2;
      height: 2.75rem;
      overflow-y: hidden;
    }
    .nav-scroller .nav {
      display: -ms-flexbox;
      display: flex;
      -ms-flex-wrap: nowrap;
      flex-wrap: nowrap;
      padding-bottom: 1rem;
      margin-top: -1px;
      overflow-x: auto;
      text-align: center;
      white-space: nowrap;
      -webkit-overflow-scrolling: touch;
    }
    .nav-scroller .nav-link {
      padding-top: .75rem;
      padding-bottom: .75rem;
      font-size: .875rem;
    }
    .card-img-right {
      height: 100%;
      border-radius: 0 3px 3px 0;
    }
    .flex-auto {
      -ms-flex: 0 0 auto;
      flex: 0 0 auto;
    }
    .h-250 { height: 250px; }
    @media (min-width: 768px) {
      .h-md-250 { height: 250px; }
    }
    .border-top { border-top: 1px solid #e5e5e5; }
    .border-bottom { border-bottom: 1px solid #e5e5e5; }
    .box-shadow { box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); }
    .blog-title {
      margin-bottom: 0;
      font-size: 2rem;
      font-weight: 400;
    }
    .blog-description {
      font-size: 1.1rem;
      color: #999;
    }
    @media (min-width: 40em) {
      .blog-title {
        font-size: 3.5rem;
      }
    }
    .blog-post {
      margin-bottom: 4rem;
    }
    .blog-post-title {
      margin-bottom: .25rem;
      font-size: 2.5rem;
    }
    .blog-post-meta {
      margin-bottom: 1.25rem;
      color: #999;
    }
    .blog-footer {
      padding: 2.5rem 0;
      color: #999;
      text-align: center;
      background-color: #f9f9f9;
      border-top: .05rem solid #e5e5e5;
    }
    .blog-footer p:last-child {
      margin-bottom: 0;
    }
If you attempt to preview the application right now you’ll get a CORS error. This happens because we are trying to fetch data from the api.acme.test domain from the acme.test domain. The request will, therefore, be rejected because we have not specified we want to share resources across these domains.

Handling CORS error in a Laravel application

To allow resource sharing from this subdomain, we will need to install a Laravel package. Open the terminal and run the following command:
    $ composer require barryvdh/laravel-cors
This will install the this Laravel CORS package. Open the app/Http/Kernel.php file and update the middlewareGroups property as seen below:
    protected $middlewareGroups = [
        // [...]

        'api' => [
            \Barryvdh\Cors\HandleCors::class,

            // [...]
        ],
    ];
Now, you can visit the web app URL http://acme.test. This should display the blog posts fetched from the API:
Building a simple admin dashboard
The last piece of the application is the admin dashboard, which will be at http://admin.acme.test. Open the routes/admin.php file and replace the contents with the following:
    <?php

    Route::view('/', 'admin.index');
Next, create a new admin directory in the resources/views directory and in there create a new index.blade.php file with the following content:
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet">
        <title>Admin</title>
    </head>
    <body>
        <div id="app">
            <div class="container">
                <div class="alert alert-success" role="alert" v-show="success" style="display: none">
                  Post added successfully.
                </div>
                <form action="" class="" v-on:submit.prevent="savePost">
                    <div class="form-group">
                        <label for="title">Post Title</label>
                        <input type="text" class="form-control" v-model="title">
                    </div>
                    <div class="form-group">
                        <label for="title">Post Content</label>
                        <textarea type="text" class="form-control" v-model="content"></textarea>
                    </div>
                    <input type="submit" value="Save Post" class="btn btn-primary">
                </form>
            </div>
        </div>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
        <script>
        new Vue({
            el: '#app',
            data: {
                title: "",
                content: "",
                success: false,
            },
            methods: {
                savePost: function () {
                    axios.post('http://api.acme.test/posts', {title: this.title, content: this.content}).then(res => {
                        this.success = true
                        this.title = this.content = ''
                        window.setTimeout(() => this.success = false, 3000)
                    })
                }
            }
        })
        </script>
    </body>
    </html>
In the HTML above, we have a form where we can enter the title and content of the post. When the post is submitted, we use Axios to send the request to the API. Since we have already added CORS support to the API earlier, we do not need to do so again and the request should work just fine.
If you visit the admin URL http://admin.acme.test you should see the admin panel:
Bonus: making the domain configurable for all environments
Right now, we hardcoded the domain into our code, which means if we wanted to work in development and production we would have to change the domain from the RouteServiceProvider every time.
To alleviate this issue, open the .env file and add a new key called APP_BASE_DOMAIN as seen below:
    APP_BASE_DOMAIN=acme.test
Next, open the config/app.php and add a new key to the config file as seen below:
    <?php

    return [

        'base_domain' => env('APP_BASE_DOMAIN'),

        // [...]

    ];
Now open the RouteServiceProvider and add the method below to the class:
    private function baseDomain(string $subdomain = ''): string
    {
        if (strlen($subdomain) > 0) {
            $subdomain = "{$subdomain}.";
        }

        return $subdomain . config('app.base_domain');
    }
Next, replace the hardcoded subdomain text in the class methods with calls to the method we just added as seen below:
    protected function mapWebRoutes()
    {
        Route::domain($this->baseDomain())
             ->middleware('web')
             ->namespace($this->namespace)
             ->group(base_path('routes/web.php'));
    }

    protected function mapAdminRoutes()
    {
        Route::domain($this->baseDomain('admin'))
             ->middleware('web')
             ->namespace($this->namespace)
             ->group(base_path('routes/admin.php'));
    }

    protected function mapApiRoutes()
    {
      Route::domain($this->baseDomain('api'))
           ->middleware('api')
           ->namespace($this->namespace)
           ->group(base_path('routes/api.php'));
    }
Now we can easily change the domain without having to modify the code.
Conclusion
In this article, we have seen how to use one Laravel codebase to handle multiple applications with ease. The source code to the application built in this article is available on GitHub.
The post Serving multiple apps with one Laravel codebase using subdomain routing appeared first on Pusher Blog.