Project Log: Creating an API with Laravel

Madeline CaplesMadeline Caples
7 min read

Introduction

Quick background: I am developing a little Latin vocabulary tool and eventually quiz generator using Vue, Inertia, and Laravel together.

Having not yet set up a database (because databases are scary), I decided to use some JSON files to store my Latin vocabulary for the time being. Now, I could just import the files directly into my Vue component and display them, but what if I wanted to dynamically display a different vocabulary list depending on which one a user selected from a menu? I figured I would have to import all of the possible JSON files in order to conditionally display one (1) of them, which seemed goofy. So instead I decided to try something else.

The solution I came up with had two steps:

  1. Passing the filenames as props to my Vue component in a controller

  2. Creating an API on the backend, that would return whichever vocabulary list I asked it for, to communicate with my Vue component

Setup

If you’d like to set up your own Vue/Inertia/Laravel project, check out my boilerplate instructions, in this article!

Step 1: passing filenames as props

There were several pieces to this step. First, I wanted to get the requested filename from the user, so I’d need both a way to display the options (a dropdown, for instance), and a way to get the options from the backend, since my Vue component doesn’t know how many JSON files I have, and what their names are on its own.

For my dropdown I used Primevue’s Select component. This accepts a prop, options, which I set equal to my lists variable - just an empty ref at this point. This is the ref I needed to fill with the options from my backend.

<script setup>
import { ref } from 'vue';
import Select from 'primevue/select';
const lists = ref([]);
</script>

<template>
    <div class="vocab-list">
        <h1>Latin Vocabulary</h1>
        <Select :options="lists" placeholder="Choose a vocab list" />
    </div>
</template>

To fill my lists variable with data, I needed to pass along the filenames of the JSON files in my public/json folder. This is where the controller came in! This code was admittedly written by chatGPT, but it does actually work.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;

class JsonFileController extends Controller
{
    public function getJsonFiles()
    {
        // Define the path to the JSON directory (Assuming it's inside public/json)
        $directory = public_path('json');

        // Check if the directory exists
        if (!File::exists($directory)) {
            return response()->json(['error' => 'Directory not found'], 404);
        }

        // Get all JSON file names from the folder
        $files = File::files($directory);

        // Extract filenames from the file objects
        $filenames = collect($files)->map(fn($file) => $file->getFilename())->all();

        // Return filenames as JSON response
        return response()->json($filenames);
    }
}

Then I used that controller inside of web.php to pass data inside my route. I got original out of vocab_names, which contained the data returned by my JsonFileController, and set that to vocab_names, which would become available to my Vue component as a prop.

use App\Http\Controllers\JsonFileController;

Route::get('/vocab', function () {
    $vocab_names = (new JsonFileController)->getJsonFiles();

    return inertia('VocabList', ['vocab_names' => $vocab_names->original]);
});

Inside of the Vue component I defined props and extracted the vocab_names. But I still needed to do a little manipulating to get the data into the right shape to be understood by my Primevue Select component.

In my script setup, inside of an onMounted hook, I used the filename value passed to props to fill my lists ref. I checked to make sure that the vocab_names prop existed and was an array, and if so I looped through it creating the shape of objects needed by Primevue to fill the options prop in the Select component. If the vocab_names did not exist I’d send a warning to the console to let myself know.

<script setup>
import { ref, onMounted } from 'vue'; 
const lists = ref([]);

onMounted(() => {
    if (props.vocab_names && Array.isArray(props.vocab_names)) { 
        for (let i = 0; i < props.vocab_names.length; i++) {
            lists.value.push({
                "name": props.vocab_names[i].replace('.json', ''),
                "code": props.vocab_names[i]
            });
        }
    } else {
        console.warn("props.vocab_names is missing or not an array");
    }
})

After getting the filename in the dropdown to choose which vocabulary list to use, I just needed a way to actually retrieve the vocabulary that matched that filename. For that I created an API, using Laravel.

Step 2: creating the API

Inside of my JsonFileController from step 1 I added a second function that would return the vocabulary instead of just the filenames. It receives the filename and dumps out the json data that corresponds to that file.

 public function getJsonFileContent($filename)
    {
        // Define the path to the JSON directory
        $directory = public_path('json');

        // Construct the full path to the JSON file
        $filePath = $directory . '/' . $filename;

        // Check if the file exists
        if (!File::exists($filePath)) {
            return response()->json(['error' => 'File not found'], 404);
        }

        // Read the content of the JSON file
        $content = File::get($filePath);

        // Decode the JSON content into an array
        $data = json_decode($content, true);

        // Return the decoded data as a JSON response
        return response()->json($data);
    }

But there was still some setup to configure my API route so that it could be exposed. I had to add my API route to an api.php file inside of the routes folder, that I created:

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\JsonFileController;

// Define API routes
Route::get('/json-files', [JsonFileController::class, 'getJsonFiles']);

// Route to get contents of a specific JSON file
Route::get('/json-files/{filename}', [JsonFileController::class, 'getJsonFileContent']);

And then in the bootstrap/app.php file I had to add this line to allow the routes in my API file to be available to the application, inside of withRouting and under web:

->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',

Inside of my Vue component I had to do a bit of maneuvering to actually retrieve the data and use it in my component. This was because I decided to componentize my vocab list as ListContent.vue and import it into my Vocabulary page, for reasons. So I needed a way to get the filename in the Select component on VocabList.vue page, then receive it inside of ListContent.vue to use that filename to look up the actual vocabulary with an API.

I decided to use Pinia to accomplish this, rather than pass props, because I knew I’d want to use the filename in several components. So I saved my vocabListID on the store and then turned it into a ref so that I could watch it for changes from inside of ListContent.vue.

import { defineStore } from 'pinia'

export const useVocabStore = defineStore('vocabStore', {
  state: () => {
    return {
        vocabListID: null,
    }
  }
})

Inside of ListContent.vue’s script setup I added:

<script setup>
import { ref, watch, onMounted } from 'vue';
import { useVocabStore } from '../stores/vocab.js';
import { storeToRefs } from 'pinia';

const vocabStore = useVocabStore();
const { vocabListID } = storeToRefs(vocabStore);

const vocab = ref({});

const getVocab = async (filename) => {
    if (!filename) return;
    try {
        const response = await fetch(`/api/json-files/${filename}`);
        vocab.value = await response.json();
        console.log('Vocab loaded:', vocab.value);
    } catch (error) {
        console.error('Error fetching vocab:', error);
    }
}

watch(vocabListID, async (newValue) => {
        console.log('VocabListID changed:', newValue);
        await getVocab(newValue);
})
</script>

So how does the above code work? I set vocab equal to a ref containing an empty object. I get vocabListID from the vocabStore as a ref and then watch that value for any changes. If the value changes, I call my getVocab function, passing the newValue (which is the filename). The getVocab function checks for the filename to exist, and if it does it tries to get a response from my API, passing the filename as a parameter to the endpoint. Then all I need is to set vocab.value equal to the json response of my API, and I can display that vocabulary inside of ListContent.vue!

Just in case you’re curious, this is how I displayed the retrieved vocabulary data (inside of another Primevue component, the Card), looping through each vocabulary item within the nouns, verbs, and prep_phrases categories of my vocabulary:

<template>
    <div class="list-content" v-if="vocab">
            <div class="nouns">
                <h2>Nouns</h2>
                <div class="vocab-section">
                    <Card class="vocab-item" v-for="(item, index) in vocab.nouns" :key="index">
                        <template #title>{{ item.nom }}</template>
                        <template #content>
                            <p>{{ item.gen }}</p>
                            <p>{{ item.translation }}</p>
                        </template>
                    </Card>
                </div>
            </div>
            <div class="verbs">
                <h2>Verbs</h2>
                <div class="vocab-section">
                    <Card class="vocab-item" v-for="(item, index) in vocab.verbs" :key="index">
                        <template #title>{{ item.first_sg }}</template>
                        <template #content>
                            <p>{{ item.infinitive }}</p>
                            <p>{{ item.translation }}</p>
                        </template>
                    </Card>
                </div>
            </div>
            <div class="prep-phrases">
                <h2>Prepositional Phrases</h2>
                <div class="vocab-section">
                    <Card class="vocab-item" v-for="(item, index) in vocab.prep_phrases" :key="index">
                        <template #title>{{ item.phrase }}</template>
                        <template #content>
                            <p>{{ item.translation }}</p>
                        </template>
                    </Card>
                </div>
            </div>
        </div>
</template>

Conclusion

And that’s how I got my vocabulary lists where I wanted them to be, using Laravel and Vue! It’s very likely there’s a better and/or easier way to achieve what I did here, and if you know of such a way, I’d love to hear about it in the comments. I am still learning, and value your input!

Resources

0
Subscribe to my newsletter

Read articles from Madeline Caples directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Madeline Caples
Madeline Caples

I'm a frontend developer at Fuego Leads where I build cool stuff using Vue. I've worked there since April 2023. On Hashnode, I like to write about machine learning and other software engineering topics.