DEV Community

pirvanm
pirvanm

Posted on

How do you make FullStack Demo App for Interviews using Laravel + React with Dockerize Part 1

Sometimes, even if the interview is only for a frontend role, I know from experience that it doesn’t take long until the discussion shifts toward full-stack. For example, at the end of one process, HR ended up asking about my experience with Go, even though the position was originally for React.
That’s why I often push myself: instead of building only the frontend in React, I also implement the backend.
Since I’m more familiar with Laravel, I usually create the backend part as well, set up the communication between the two, and even prepare a Dockerized version.
How are interviews usually structured for you?
I completed this practical test for a specific country and company in Europe.

This project feels like a Middle–Senior level challenge, so I plan to split it into 2 main parts.

Each Main Part will then be divided into 4–5 subparts to keep the learning and implementation process structured.

For example:

Main Part 1
→ Subpart 1 (since we need to start working with dynamic data, this will be the first step).

In project backend/ in folder database which have /migrations/ i made :

create_mentors_table.php
create_mentor_applications_table.php
create_mentorships_table.php
_create_personal_access_tokens_table.php

create_mentors_table.php

public function up(): void
    {
        Schema::create('mentors', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('user_id');
            $table->string('title');
            $table->text('bio');
            $table->text('technical_bio');
            $table->text('mentoring_style');
            $table->text('audience');
            $table->string('avatar')->nullable();
            $table->json('expertise');
            $table->string('availability');
            $table->timestamps();


            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
            $table->index('user_id');
        });
    }

Enter fullscreen mode Exit fullscreen mode

create_mentor_applications_table.php

Schema::create('mentor_applications', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('mentor_id'); // foreign to user_id
            $table->unsignedBigInteger('mentee_id'); // foreign to user_id
            $table->text('message');
            $table->string('status'); // pending, accepted, rejected, withdrawn
            $table->timestamp('responded_at')->nullable();
            $table->timestamps();
        });

Enter fullscreen mode Exit fullscreen mode
create_mentorships_table.php

` public function up(): void
    {
        Schema::create('mentorships', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('mentor_id'); // foreign to user_id
            $table->unsignedBigInteger('mentee_id'); // foreign to user_id
            $table->timestamp('started_at');
            $table->timestamp('ended_at')->nullable();
            $table->string('status'); // active, completed, terminated
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('mentorships');
    }
Enter fullscreen mode Exit fullscreen mode

In file personal_access_tokens_table.php i write :


Schema::create('personal_access_tokens', function (Blueprint $table) {
            $table->id();
            $table->morphs('tokenable');
            $table->text('name');
            $table->string('token', 64)->unique();
            $table->text('abilities')->nullable();
            $table->timestamp('last_used_at')->nullable();
            $table->timestamp('expires_at')->nullable()->index();
            $table->timestamps();
        });

Enter fullscreen mode Exit fullscreen mode

database/factories
/ MentorDatabaseFactory.php

 public function create(array $overrides = []): int
    {
        $data = array_merge([
            'user_id' => $overrides['user_id'] ?? throw new \InvalidArgumentException('user_id is required'),
            'title' => fake()->jobTitle(),
            'bio' => fake()->paragraph(3),
            'technical_bio' => fake()->paragraph(3),
            'mentoring_style' => fake()->paragraph(3),
            'audience' => fake()->paragraph(3),
            'avatar' => fake()->optional(0.7)->imageUrl(300, 300, 'people'),
            'expertise' => json_encode(fake()->words(random_int(3, 6))),
            'availability' => fake()->randomElement(['open', 'limited']),
            'created_at' => now(),
            'updated_at' => now(),
        ], $overrides);

        return DB::table('mentors')->insertGetId($data);

Enter fullscreen mode Exit fullscreen mode

UserDatabaseFactory.php

class UserDatabaseFactory
{
    public function create(array $overrides = []): int
    {
        $firstName = $overrides['first_name'] ?? fake()->firstName();
        $lastName = $overrides['last_name'] ?? fake()->lastName();

        $data = array_merge([
            'first_name' => $firstName,
            'last_name' => $lastName,
            'email' => $overrides['email'] ?? strtolower("{$firstName}.{$lastName}") . '@example.com',
            'password' => $overrides['password'] ?? bcrypt('password'),
            'email_verified_at' => now(),
            'created_at' => now(),
            'updated_at' => now(),
        ], $overrides);

        return DB::table('users')->insertGetId($data);
    }

Enter fullscreen mode Exit fullscreen mode

database/seeders
/DatabaseSeeder.php

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        $userFactory = new UserDatabaseFactory();
        $mentorFactory = new MentorDatabaseFactory();

        $mentorsData = [
            [
                'first_name' => 'Sarah',
                'last_name' => 'Johnson',
                'title' => 'Senior Laravel Developer',
                'expertise' => ['laravel', 'php', 'testing', 'ddd'],
                'availability' => 'open',
                'avatar' => 'https://randomuser.me/api/portraits/men/45.jpg',
            ],
            [
                'first_name' => 'Michael',
                'last_name' => 'Chen',
                'title' => 'Full-Stack Engineer & Mentor',
                'expertise' => ['vue', 'javascript', 'api', 'architecture'],
                'availability' => 'open',
                'avatar' => 'https://randomuser.me/api/portraits/women/45.jpg',
            ],
            [
                'first_name' => 'Amina',
                'last_name' => 'Khalid',
                'title' => 'Backend Specialist',
                'expertise' => ['php', 'symfony', 'laravel', 'microservices'],
                'availability' => 'limited',
                'avatar' => 'https://randomuser.me/api/portraits/men/45.jpg',
            ],
            [
                'first_name' => 'James',
                'last_name' => 'Wilson',
                'title' => 'DevOps & Laravel CI/CD',
                'expertise' => ['docker', 'ci/cd', 'aws', 'deployment'],
                'availability' => 'open',
                'avatar' => 'https://randomuser.me/api/portraits/men/45.jpg',
            ],
            [
                'first_name' => 'Elena',
                'last_name' => 'Martinez',
                'title' => 'UX Engineer & Mentor',
                'expertise' => ['ux', 'figma', 'tailwind', 'frontend'],
                'availability' => 'limited',
                'avatar' => 'https://randomuser.me/api/portraits/men/45.jpg',
            ],
            [
                'first_name' => 'David',
                'last_name' => 'Park',
                'title' => 'Laravel Educator',
                'expertise' => ['teaching', 'laravel', 'best practices', 'security'],
                'availability' => 'open',
                'avatar' => 'https://randomuser.me/api/portraits/women/45.jpg',
            ],
        ];

        foreach ($mentorsData as $data) {
            $userId = $userFactory->create([
                'first_name' => $data['first_name'],
                'last_name' => $data['last_name'],
            ]);

            $mentorFactory->create([
                'user_id' => $userId,
                'title' => $data['title'],
                'expertise' => json_encode($data['expertise']),
                'availability' => $data['availability'],
                'avatar' => $data['avatar']
            ]);
        }
    }

Enter fullscreen mode Exit fullscreen mode

✨ Even though I started my career with Laravel 4 and have worked with every version since, I was very curious about Laravel 12. That curiosity pushed me to dive deeper into Domain-Driven Design (DDD), and I’ve been learning and practicing it.

✅ For now, I feel comfortable enough with the fundamentals, so I’m ready to move on to the next step. This will be Subpart 2 of Main Part 1: Domain, where I’ll continue exploring with some practical examples.

This way, it flows naturally:

You highlight your experience.

You show curiosity and learning (DDD).

As business logic we can split this app in 2 part Mentor and User :

on user next steps is this folder structure :
backend/app/Domain
/User/

backend/app/Domain/User/Entities
/User.php

I think the model is the most common part of every Laravel app, and now we can quickly see the differences in models within a modern Laravel 12 application.

<?php

namespace App\Domain\User\Entities;

use App\Domain\User\ValueObjects\Email;
use App\Domain\User\ValueObjects\Password;

class User
{
    public function __construct(
        private int $id,
        private string $first_name,
        private string $last_name,
        private Email $email,
        private Password $password
    ) {}

    public function id(): int
    {
        return $this->id;
    }

    public function firstName(): string
    {
        return $this->first_name;
    }

    public function lastName(): string
    {
        return $this->last_name;
    }

    public function email(): Email
    {
        return $this->email;
    }

    public function password(): Password
    {
        return $this->password;
    }
}


Enter fullscreen mode Exit fullscreen mode

app/Domain/User/Factories
/UserFactoryInterface.php

<?php

namespace App\Domain\User\Factories;

use App\Application\User\DTOs\UserDTO;
use App\Domain\User\Entities\User;

interface UserFactoryInterface
{
    public function from(object|array $data): Entity;
}

Enter fullscreen mode Exit fullscreen mode

backend/app/Domain/User/Factories
/UserValueObjectsFactoryInterface.php

<?php

namespace App\Domain\User\Factories;

use App\Domain\User\ValueObjects\Email;
use App\Domain\User\ValueObjects\Password;

interface UserValueObjectsFactoryInterface
{
    public function email(string $value): Email;
    public function password(string $value): Password;
}

Enter fullscreen mode Exit fullscreen mode

backend/app/Domain/User/Repositories
/UserRepositoryInterface.php

<?php

namespace App\Domain\User\Repositories;

use App\Domain\User\Entities\User;
use App\Domain\User\ValueObjects\Email;

interface UserRepositoryInterface
{
    public function create(array $data): User;
    public function findByEmail(Email $email): ?User;
    public function findById(int $id): ?User;
}

Enter fullscreen mode Exit fullscreen mode

backend/app/Domain/User/ValueObjects
/Email.php

<?php

namespace App\Domain\User\ValueObjects;

use InvalidArgumentException;

class Email
{
    public function __construct(private string $value)
    {
        if (! filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException("Invalid email: {$value}");
        }
        $this->value = strtolower(trim($value));
    }

    public function value(): string
    {
        return $this->value;
    }

    public function equals(self $other): bool
    {
        return $this->value() === $other->value();
    }

    public function __toString(): string
    {
        return $this->value();
    }
}

Enter fullscreen mode Exit fullscreen mode

app/Domain/User/ValueObjects
/Password.php

<?php

namespace App\Domain\User\ValueObjects;

use InvalidArgumentException;

class Password
{
    public function __construct(private string $value)
    {
        if (strlen($value) < 8) {
            throw new InvalidArgumentException('Password must be at least 8 characters.');
        }
    }

    public function value(): string
    {
        return $this->value;
    }

    public function matches(string $plainText): bool
    {
        return password_verify($plainText, $this->value);
    }

    public static function hash(string $plainText): self
    {
        return new self(password_hash($plainText, PASSWORD_DEFAULT));
    }
}

Enter fullscreen mode Exit fullscreen mode

🛠 Maybe because 4 months ago I worked with the Symfony Framework, and since I’m a full-stack developer, I often work with different technologies.

In 2023, I spent some time with CodeIgniter, while most of my frontend work was with Vue.js. Occasionally, I also worked with React. Unfortunately, in 2024, I had to spend a lot of time using jQuery (because of company requirements).

Now, I feel a bit unsure about what the best practices in Laravel are today. 🤔
👉 Any feedback or advice from the community would be really welcome.

After this, I will continue with the rest of the DDD implementation in the Laravel 12 app.

If you like my work, feel free to follow me on the web or on LinkedIn at LevelCoding.”

And I used AI only to correct my grammatical phrases — the code is entirely thought out by me.

Top comments (0)