0

I’m running a Laravel backend alongside a Vue frontend on NGINX. The issue I’m facing is that my API endpoints are returning HTML instead of JSON. For example:

  • https://isuecampusmap.site/api/ → returns JSON correctly (health check works)

  • https://isuecampusmap.site/api/buildings → returns HTML or “File not found”

Here’s what I’ve already tried:

  • Verified that routes/api.php contains the correct routes (/api/buildings, /api/rooms, etc.).

  • Confirmed that public/index.php exists in the Laravel backend.

  • Cleared Laravel caches (php artisan config:clear, route:clear, cache:clear).

  • Adjusted NGINX config to separate Vue (/) and Laravel (/api).

  • Tested with curl -H "Accept: application/json" but still get HTML for most routes.

Question: Why are my Laravel API routes (like /api/buildings) being served as HTML instead of JSON? Is this an NGINX misconfiguration, or is Sanctum middleware blocking unauthenticated requests? What’s the correct way to configure NGINX so that all /api/* routes are passed to Laravel’s index.php and return JSON responses?

Any guidance or working config examples would be greatly appreciated.

Environment:

  • Laravel 11.27.2

  • PHP 8.3.6

  • NGINX 1.24.0 (Ubuntu)

  • Ubuntu 24.04 LTS

My NGINX config:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name isuecampusmap.site www.isuecampusmap.site;

    ssl_certificate /etc/letsencrypt/live/isuecampusmap.site/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/isuecampusmap.site/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # Vue Frontend
    root /var/www/isuecampusmap/isueadminpanelnewvue/dist;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    # Laravel API
    location /api/ {
        alias /var/www/isuecampusmap/backend/public/;
        index index.php;

        try_files $uri $uri/ /index.php?$query_string;

        location ~ \.php$ {
            include fastcgi_params;
            fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
            fastcgi_param SCRIPT_FILENAME $request_filename;
            fastcgi_param SCRIPT_NAME $fastcgi_script_name;
        }
    }

    # Cache static assets
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Deny hidden files
    location ~ /\.(?!well-known) {
        deny all;
    }
}
New contributor
Bunz is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
4
  • It's your host. Hostinger likes to add javascript checks on all routes. There may be a way to turn it off, but you may need to contact Hostinger about that Commented 2 days ago
  • Otherwise, you might need to add a json response header, since it works for the /api route. Commented 2 days ago
  • Thanks for the suggestion! In my case this is a Hostinger VPS, not shared hosting, so they don’t inject JavaScript checks automatically. The issue seems to be with my NGINX config — /api/ sometimes returns raw PHP or 403, and /api/buildings falls back to the Vue frontend. I’m working on adjusting the alias + SCRIPT_FILENAME mapping so PHP‑FPM consistently executes Laravel’s index.php. Commented 2 days ago
  • I inspected the response when it was working, it was most definitely returning Hostinger's "you must enable javascript" response. However, it's now a 404 File Not Found, so back to nginx. Especially since it's not returning Laravel's 404 (stylized or otherwise), so nginx is not routing the request properly back to Laravel. You might want to check nginx's logs to see if there's any hint to where it's being routed. Commented 2 days ago

1 Answer 1

1

Your first mistake is the incorrect usage of the try_files directive. The very last try_files directive argument has a completely different meaning from the preceding ones and specifies the fallback action, which can be an HTTP response status code, a fallback URI (which you are attempting to use), or the fallback named location name. Crucially, after the internal redirect performed by nginx according to your /index.php?$query_string fallback URI, a search for the most suitable location to handle the rewritten URI will be performed again. Evidently, this will not be your location /api/ { ... } block. The HTML response you are receiving is most likely the "invalid route" page from your frontend Vue.js application.

If you were using the root directive inside your location /api { ... }, it would be enough to correct this fallback URI by adding your API location prefix to it:

try_files $uri $uri/ /api/index.php?$query_string;

However, since you are using the alias directive instead of root, and your fallback URI contains variables, such a change will be insufficient due to the peculiarities of using the alias and try_files directives together, as described in this nginx trac ticket.

The simplest way to circumvent this peculiarity/bug is to use the suggestion from the most upvoted answer to the "NGINX try_files + alias directives" question, duplicating the /api/ prefix specified for the outer location directive in the fallback URI:

location /api/ {
    ...
    try_files $uri $uri/ /api//api/index.php?$query_string;

(Regardless of how strange this solution might look at first glance.)

Your second potential mistake is using the try_files directive at all within a location intended for an API controller.

This entire configuration snippet is likely a blind copy of a generic nginx configuration example for a typical PHP application. If the responses to your API requests do not involve serving static files as the response bodies, but should be processed exclusively by the central /var/www/isuecampusmap/backend/public/index.php controller (which I'm 99% certain is the case), then you do not need this entire complex and verbose construction. All you need in this scenario is to process all requests using index.php. The configuration for this is significantly simpler (and performs better):

location /api/ {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME /var/www/isuecampusmap/backend/public/index.php;
    fastcgi_param DOCUMENT_ROOT /var/www/isuecampusmap/backend/public;
    fastcgi_param SCRIPT_NAME /index.php;
    fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
}

You might still be wondering why the https://isuecampusmap.site/api/ request succeeds using your configuration while every other request fails? The reason is that in this specific case, the try_files fallback argument is not engaged. As stated in the try_files directive documentation:

Checks the existence of files in the specified order and uses the first found file for request processing; the processing is performed in the current context. The path to a file is constructed from the file parameter according to the root and alias directives. It is possible to check directory’s existence by specifying a slash at the end of a name, e.g. “$uri/”.

The /api/ request maps to the /var/www/isuecampusmap/backend/public/ physical directory, which does exist, so the second check performed by the try_files directive (according to the $uri/ argument) succeeds. The try_files directive allows this request to be processed within this (outer) location. Next, the index index.php directive finds the index.php file within this directory and performs an internal redirect to /api/index.php. This behavior, although it may be surprising to inexperienced users, is explicitly documented:

It should be noted that using an index file causes an internal redirect, and the request can be processed in a different location.

Then, as already mentioned, a search for the most suitable location to handle the rewritten request will be performed again, and this time it truly will be the inner location ~ \.php$ { ... }.

Assuming the index.php file is the only file within the /var/www/isuecampusmap/backend/public/ directory, the only other request that could be handled by this location is /api/index.php (directly matching this inner location), though I doubt the API controller will be happy to see /api/index.php as a route.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.