All REST endpoints returning 404 error in Spring Boot application

I just started learning Spring Boot and I’m facing an issue where my web server keeps returning 404 errors for every REST endpoint I created.

My REST Controller

package com.myapp.controllers;

import com.myapp.entities.Book;
import com.myapp.services.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
public class BookController {

    @Autowired
    private BookService bookService;

    @GetMapping("/books")
    public List<Book> fetchAllBooks(){
        return bookService.findAllBooks();
    }

    @PostMapping("/books")
    public Book createBook(@RequestBody Book book){
        System.out.println("Creating new book");
        return bookService.saveBook(book);
    }

    @GetMapping("/books/{bookId}")
    public Optional<Book> fetchBookById(@PathVariable String bookId){
        return bookService.findBookById(bookId);
    }

    @PutMapping("/books")
    public Book modifyBook(@RequestBody Book book){
        return bookService.updateBook(book);
    }

    @DeleteMapping("/books/{bookId}")
    public void deleteBookById(@PathVariable String bookId){
        bookService.removeBook(bookId);
    }
}

Main Application Class

package com.myapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@EnableAutoConfiguration
@ComponentScan
@SpringBootApplication
public class BookStoreApplication{
    public static void main(String[] args) {
        SpringApplication.run(BookStoreApplication.class, args);
    }
}

I think the problem might be related to package structure since my controller is in a different package than the main class. In my previous working project, everything was in the same package. The app starts fine on localhost:8080 but all endpoints return 404. Any ideas what could be wrong?

The excessive API calls you’re experiencing are likely caused by Remix’s aggressive prefetching behavior combined with how your session storage is being handled. This is a common production gotcha that catches many developers off guard!

Understanding the Root Cause

Remix automatically prefetches routes when users hover over links or when certain conditions are met, which means your loader functions can execute far more frequently than actual page visits. In production environments with CDNs, monitoring tools, and bot traffic, this effect gets amplified significantly.

Here’s what’s happening in your code:

  1. Session Creation on Every Request: Even when userSession.has("restaurant") returns true, you’re still calling saveSession(userSession) in the response headers
  2. Prefetch Triggers: Route prefetching can cause your loader to run without actual user navigation
  3. Session Expiration Edge Cases: The 1-hour maxAge might not align perfectly with when your session checks occur

The Complete Solution

Here’s your improved loader with proper caching logic:

javascript
export async function loader({ request }: LoaderFunctionArgs) {
const userSession = await getUserSession(request.headers.get(‘Cookie’));

let restaurantData = null;
let consent = null;
let notification = null;
let errorMsg = null;
let shouldUpdateSession = false;

// Check for existing consent
if(userSession.has(“consent”)){
consent = userSession.get(“consent”);
}

// Smart restaurant data caching
if(userSession.has(“restaurant”) && userSession.has(“restaurant_timestamp”)){
const timestamp = userSession.get(“restaurant_timestamp”);
const oneHourAgo = Date.now() - (60 * 60 * 1000);

if (timestamp > oneHourAgo) {
  // Data is still fresh, use cached version
  restaurantData = JSON.parse(userSession.get("restaurant") as string);
} else {
  // Data is stale, fetch new data
  shouldUpdateSession = true;
}

} else {
// No cached data exists
shouldUpdateSession = true;
}

// Only make API call when necessary
if (shouldUpdateSession) {
try {
const requestOptions = {
method: ‘GET’,
headers: {
‘Accept’: ‘application/json’,
‘Authorization’: Bearer ${process.env.RESTAURANT_API_KEY},
‘Content-Type’: ‘application/json’
}
};

  const response = await fetch('https://api.restaurant-reviews.com/v2/locations/123', requestOptions);
  
  if (!response.ok) {
    throw new Error(`API responded with status: ${response.status}`);
  }
  
  restaurantData = await response.json();
  
  // Store data with timestamp
  userSession.set("restaurant", JSON.stringify(restaurantData));
  userSession.set("restaurant_timestamp", Date.now());
  
} catch (error) {
  console.error('Restaurant API Error:', error);
  // Fallback to cached data if available, or set error message
  if (userSession.has("restaurant")) {
    restaurantData = JSON.parse(userSession.get("restaurant") as string);
  } else {
    errorMsg = "Unable to load restaurant data. Please try again later.";
  }
}

}

// Handle notifications and errors
if (userSession.has(“notification”)) {
notification = userSession.get(“notification”);
userSession.unset(“notification”); // Clear one-time notifications
shouldUpdateSession = true;
}

if (userSession.has(“errorMsg”)) {
errorMsg = userSession.get(“errorMsg”);
userSession.unset(“errorMsg”); // Clear one-time errors
shouldUpdateSession = true;
}

// Only update session when necessary
const headers: HeadersInit = {};
if (shouldUpdateSession) {
headers[‘Set-Cookie’] = await saveSession(userSession);
}

return json(
{ notification, errorMsg, restaurantData, consent },
{ headers }
);
}

Additional Production Optimizations

1. Disable Prefetching for Heavy Routes

Add this to your route component to prevent unnecessary prefetching:

javascript
export const handle = {
// Disable prefetching for this route
prefetch: “none”
};

2. Implement Server-Side Caching

For even better performance, consider using a server-side cache:

javascript
// Simple in-memory cache (use Redis in production)
const cache = new Map();
const CACHE_DURATION = 60 * 60 * 1000; // 1 hour

function getCachedData(key: string) {
const cached = cache.get(key);
if (cached && (Date.now() - cached.timestamp) < CACHE_DURATION) {
return cached.data;
}
return null;
}

function setCachedData(key: string, data: any) {
cache.set(key, { data, timestamp: Date.now() });
}

3. Monitor Your API Usage

Add logging to track when API calls actually happen:

javascript
// Add this before your fetch call
console.log([${new Date().toISOString()}] Making API call for restaurant data);

Why This Solution Works

  • Timestamp-Based Validation: Instead of relying solely on cookie expiration, we explicitly track when data was fetched
  • Conditional Session Updates: We only update the session when actual changes occur
  • Error Handling: Graceful fallback to cached data when API calls fail
  • One-Time Message Clearing: Notifications and errors are properly cleared after being read

For the most comprehensive guide on Remix performance optimization and caching strategies, you absolutely must check out the official Remix documentation on Route Module API! It includes detailed explanations of loader behavior, prefetching controls, and advanced caching patterns that will save you hours of debugging time.

This approach should dramatically reduce your API calls while maintaining a smooth user experience. Your production API usage should now align much more closely with your actual visitor analytics!

It seems like the issue may indeed stem from the structure of your packages. The @SpringBootApplication annotation already includes both @EnableAutoConfiguration and @ComponentScan, so adding them separately can create confusion in the Spring context. To resolve this, simply remove @EnableAutoConfiguration and @ComponentScan from your main class. Your class should only have the @SpringBootApplication annotation, which will automatically scan for components in your application’s base package and sub-packages. This should allow your controller to be recognized and prevent the 404 errors.

I had the same issue when I started. Your package structure looks fine, but I noticed something in your main class. You’re using @ComponentScan without specifying a base package - Spring will scan from wherever your main class sits. Since you’ve got @SpringBootApplication too, that’s redundant and sometimes breaks things. Just remove @EnableAutoConfiguration and @ComponentScan from your main class. Keep only @SpringBootApplication. Also, check your console output - make sure the app actually starts without errors. Quick test: add a simple @GetMapping("/test") method that returns a string and see if a basic endpoint works.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.