Lesson 2 of 8
Integrating the Job Search API
Estimated time: 2.5–3 hours
What You Will Learn
- What a third-party API is and why we use APIs instead of web scraping
- How to build a service class in Spring Boot that calls an external API
- How to use
RestTemplateto make HTTP requests from Java - How to securely inject API keys using
application.properties - How to build a controller that exposes a search endpoint for your frontend
- How to read and understand a real JSON response from a job search API
- How to handle errors and API downtime gracefully
Part 1 — Your Server Talking to Another Server
In Lessons 12 and 13, you built your first Spring Boot application. You created controllers that handled requests from a web browser, and you sent JSON responses back. That was a huge milestone — you built a real, working API. But in those lessons, all the data either came from the user (like contact form submissions) or was hardcoded in your Java code. Your server was self-contained. It did not need to talk to anything else on the internet to do its job.
Today, everything changes. In this lesson, your Spring Boot server is going to reach out across the internet and talk to another server. Specifically, it is going to call a real job search API to find actual job listings based on a search query and location. Your server becomes a middleman — the browser asks your server for jobs, your server asks the job search API for jobs, the job search API responds with data, and your server passes that data back to the browser. This is called third-party API integration, and it is one of the most common patterns in professional software development.
Think about the apps you use every day. When a weather app shows you the forecast, it is calling a weather API. When a travel site shows you flight prices, it is calling airline APIs. When a food delivery app shows restaurants near you, it is calling a maps API and a restaurant API. Almost every modern application is built by combining multiple APIs together. After this lesson, you will know how to do that yourself.
Part 2 — What Is a Third-Party API?
A third-party API is simply an API built and maintained by someone else that you call from your own code. The word "third-party" means it is not built by you (first party) or by your user (second party) — it is built by a third party, some other company or developer. You do not own the data, you do not manage the servers, and you do not write the code that powers it. You just make HTTP requests to it and get data back.
For our Resumator project, we are going to use an API called JSearch, which is available through a platform called RapidAPI. JSearch aggregates job listings from across the internet — from company career pages, job boards, and other sources — and makes them available through a clean, simple API. You send it a job title and a location, and it sends back a list of real, current job openings. These are not fake listings or sample data. These are real jobs that real companies are hiring for right now.
Why Use an API Instead of Scraping?
You might be wondering: "Why not just write code that visits job websites and reads the HTML directly?" That approach is called web scraping, and while it technically works, it has serious problems:
- It breaks constantly. Websites change their HTML structure all the time. A scraper that works today might be completely broken tomorrow because a company redesigned their job listings page.
- It is usually against the rules. Most websites explicitly forbid scraping in their Terms of Service. Violating these terms can get your IP address blocked or even lead to legal trouble.
- It is slow and fragile. Scraping requires downloading entire HTML pages, parsing through all the markup to find the data you want, and handling inconsistent formatting. APIs give you clean, structured data instantly.
- It does not scale. If you need to search across dozens of job boards, you would need a separate scraper for each one. An aggregation API like JSearch does all of that work for you behind the scenes.
APIs are the professional, reliable, and legal way to get data from other services. This is how it is done in the real world, and learning to work with third-party APIs is a critical skill for any developer.
To use the JSearch API, you will need to sign up for a free account on RapidAPI and subscribe to the JSearch API. The free tier gives you a limited number of requests per month, which is more than enough for development and learning. When you subscribe, RapidAPI gives you an API key — a long string of characters that identifies you. Every time your code calls the API, you include this key so the API knows who is making the request.
application.properties, and you should add that file to your .gitignore to keep it out of version control. In a production application, you would use environment variables or a secrets management service instead.
Once you have your RapidAPI account and have subscribed to JSearch, find your API key on the JSearch API page. It will be a long alphanumeric string. Copy it — you will need it in a moment.
First, add your API key to your project's configuration. Open the file src/main/resources/application.properties and add this line:
# JSearch API configuration
rapidapi.key=YOUR_RAPIDAPI_KEY_HERE
rapidapi.host=jsearch.p.rapidapi.com
Replace YOUR_RAPIDAPI_KEY_HERE with the actual API key you copied from RapidAPI. Spring Boot automatically reads this file when your application starts, and it makes these values available to your Java code through a mechanism called property injection. You will see how that works in the next section.
Part 3 — Building JobSearchService.java
In Lessons 12 and 13, you put all of your logic directly inside the controller class. That works fine for simple applications, but as your code grows, it is important to separate concerns. The controller's job should be limited to handling HTTP requests and responses — it should not contain business logic or API call details. The actual work of calling the JSearch API belongs in a dedicated service class.
A service class is a Java class whose sole purpose is to perform a specific piece of business logic. In our case, that logic is "call the JSearch API and return the results." The controller will delegate to the service, and the service will do the heavy lifting. This separation makes your code easier to read, easier to test, and easier to maintain.
Create a new file called JobSearchService.java in your project's source folder (in the same package as your main application class). Here is the complete code:
package com.example.resumator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class JobSearchService {
@Value("${rapidapi.key}")
private String apiKey;
@Value("${rapidapi.host}")
private String apiHost;
private final RestTemplate restTemplate = new RestTemplate();
public String searchJobs(String query, String location, int page) {
// Build the URL with query parameters
String url = "https://jsearch.p.rapidapi.com/search"
+ "?query=" + query + " in " + location
+ "&page=" + page
+ "&num_pages=1";
// Set the required headers for RapidAPI authentication
HttpHeaders headers = new HttpHeaders();
headers.set("X-RapidAPI-Key", apiKey);
headers.set("X-RapidAPI-Host", apiHost);
// Create the request entity (headers only, no body for GET requests)
HttpEntity<String> entity = new HttpEntity<>(headers);
// Make the GET request to JSearch API
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
String.class
);
// Return the JSON response body
return response.getBody();
}
}
This is a critical file, so let us go through it line by line. Understanding every piece here will give you the skills to integrate any third-party API in the future, not just JSearch.
The @Service Annotation
@Service — This annotation at the top of the class tells Spring Boot: "This class is a service component. Please create an instance of it and manage its lifecycle." This is similar to @RestController, which you learned in Lesson 12. The difference is that @RestController marks a class that handles HTTP requests, while @Service marks a class that contains business logic. Spring Boot automatically discovers classes with @Service and makes them available for other classes to use (this is called dependency injection, and you will see it in action when we build the controller).
The @Value Annotation for API Key Injection
@Value("${rapidapi.key}")
private String apiKey;
@Value("${rapidapi.host}")
private String apiHost;
@Value("${rapidapi.key}") — This is property injection. Remember the application.properties file where you stored your API key? The @Value annotation tells Spring Boot: "When you create this class, go look up the property called rapidapi.key in the configuration files and assign its value to this field." The ${...} syntax is called a property placeholder — Spring Boot replaces it with the actual value at runtime.
This is why we do not hardcode the API key in our Java code. If you ever need to change the key (for example, when rotating credentials or switching to a production key), you only update application.properties. You do not have to touch any Java code at all. This is a fundamental best practice in professional software development.
RestTemplate for HTTP Calls
private final RestTemplate restTemplate = new RestTemplate();
RestTemplate is a class provided by Spring that makes it easy to send HTTP requests from Java code. Think of it as the Java equivalent of the fetch() function you used in JavaScript. Just like fetch() lets a browser send requests to a server, RestTemplate lets your server send requests to other servers. It handles all the low-level details of opening network connections, sending headers, reading response bodies, and managing timeouts.
We create a single RestTemplate instance and reuse it for every API call. This is more efficient than creating a new one each time.
Building the URL
String url = "https://jsearch.p.rapidapi.com/search"
+ "?query=" + query + " in " + location
+ "&page=" + page
+ "&num_pages=1";
This constructs the URL we will send to the JSearch API. Let us break it apart:
https://jsearch.p.rapidapi.com/search— This is the base URL of the JSearch search endpoint. Just like your Spring Boot API has endpoints at specific paths (like/api/jobs/search), the JSearch API has its own endpoints. This is the one that searches for jobs.?query=— The question mark begins the query parameters. These are key-value pairs appended to the URL that provide input to the API. Thequeryparameter tells JSearch what kind of job to search for. We combine the user's search term with their location using the format "java developer in Lansing Michigan."&page=— The ampersand separates additional query parameters. Thepageparameter controls pagination — which page of results to return. If there are 100 results and each page shows 10, page 1 gives you results 1–10, page 2 gives you 11–20, and so on.&num_pages=1— This tells JSearch to return one page of results at a time. We keep this simple for now.
Setting the Authentication Headers
HttpHeaders headers = new HttpHeaders();
headers.set("X-RapidAPI-Key", apiKey);
headers.set("X-RapidAPI-Host", apiHost);
HTTP headers are metadata that travel along with every HTTP request. They provide extra information about the request to the server. In this case, the JSearch API (through RapidAPI) requires two specific headers for authentication:
X-RapidAPI-Key— This is your API key. It proves that you are a registered user with permission to call this API. TheapiKeyvariable gets its value fromapplication.propertiesvia the@Valueannotation we discussed earlier.X-RapidAPI-Host— This identifies which specific API on the RapidAPI platform you are calling. RapidAPI hosts thousands of APIs, and this header tells it to route your request to JSearch specifically.
If you remember from Lesson 13, when we used JavaScript fetch() to call our own API, we set the Content-Type header to tell the server that we were sending JSON. The concept here is exactly the same — headers communicate important information. The difference is that instead of describing the data format, these headers provide authentication credentials.
Making the GET Request
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
String.class
);
This is where the actual HTTP call happens. Let us break it down:
HttpEntity<String> entity— AnHttpEntitypackages together the headers (and optionally a request body) into a single object thatRestTemplatecan use. Since we are making a GET request, there is no body — we only need the headers. Think of it as an envelope: the headers are the address label, and the body (if we had one) would be the letter inside.restTemplate.exchange()— This is the method that actually sends the HTTP request over the internet. It takes four arguments:url— The URL to send the request to (the JSearch endpoint we built above).HttpMethod.GET— The HTTP method to use. We want to read data from JSearch, so we use GET.entity— TheHttpEntitycontaining our authentication headers.String.class— The type we want the response body to be converted to. We ask for a rawStringbecause we are going to pass the JSON response directly to the browser without parsing it in Java.
ResponseEntity<String> response— Theexchange()method returns aResponseEntitythat contains everything about the response: the status code, the headers, and the body. We care about the body, which contains the JSON job listings.
Finally, response.getBody() extracts just the body (the JSON string) from the response and returns it. This JSON will contain all the job listings matching the search query.
The Big Picture of RestTemplate
When you used fetch() in JavaScript (Lesson 13), your browser was making HTTP requests to your Spring Boot server. Now with RestTemplate, your Spring Boot server is making HTTP requests to the JSearch server. The concept is identical — HTTP requests and responses — but the direction is different. Your server is now acting as both a server (to your browser) and a client (to JSearch).
Part 4 — Building JobSearchController.java
With the service class complete, we now need a controller to expose a search endpoint that the browser can call. The controller's job is simple: receive the search parameters from the browser, pass them to the service, and return whatever the service gives back. It acts as a thin layer between the frontend and the service logic.
Create a new file called JobSearchController.java in the same package:
package com.example.resumator;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/api/jobs")
public class JobSearchController {
private final JobSearchService jobSearchService;
// Constructor injection — Spring automatically provides the service
public JobSearchController(JobSearchService jobSearchService) {
this.jobSearchService = jobSearchService;
}
@GetMapping("/search")
public String searchJobs(
@RequestParam String query,
@RequestParam String location,
@RequestParam(defaultValue = "1") int page
) {
return jobSearchService.searchJobs(query, location, page);
}
}
This controller is short, and that is a good thing. A well-designed controller should be thin — it should delegate the real work to service classes. Let us walk through each part.
The Class-Level Annotations
@CrossOrigin(origins = "*") — You saw this in Lesson 13 when we discussed the CORS problem. This annotation tells Spring Boot to allow requests from any origin. Since the Resumator frontend will eventually run in a browser and may be served from a different port or domain than the API, we need this to avoid CORS errors. Remember, in production you would replace "*" with the specific URL of your frontend.
@RestController — Same as in Lesson 12. This marks the class as a REST controller that sends and receives data (typically JSON) rather than HTML pages.
@RequestMapping("/api/jobs") — This is a new annotation. It sets a base path for all endpoints in this controller. Every @GetMapping, @PostMapping, etc. inside this class will have /api/jobs prepended to its path. So when we write @GetMapping("/search") below, the full URL becomes /api/jobs/search. This is a convenient way to organize your API paths and avoid repeating the base path on every method.
Constructor Injection
private final JobSearchService jobSearchService;
public JobSearchController(JobSearchService jobSearchService) {
this.jobSearchService = jobSearchService;
}
This is dependency injection in action. Remember how we marked JobSearchService with the @Service annotation? That told Spring Boot to create and manage an instance of that class. Here, we are telling Spring Boot: "When you create the JobSearchController, I need you to provide me with an instance of JobSearchService." Spring Boot sees the constructor parameter, finds the managed JobSearchService instance, and passes it in automatically.
This is a core Spring Boot concept. Instead of creating objects yourself with new JobSearchService(), you let Spring Boot create them and hand them to you. This makes your code more flexible and easier to test, because you can swap out implementations without changing the controller.
The Search Endpoint
@GetMapping("/search")
public String searchJobs(
@RequestParam String query,
@RequestParam String location,
@RequestParam(defaultValue = "1") int page
) {
return jobSearchService.searchJobs(query, location, page);
}
@GetMapping("/search") — Combined with the class-level @RequestMapping("/api/jobs"), this creates an endpoint at /api/jobs/search that responds to HTTP GET requests.
@RequestParam — This is how you read query parameters from the URL. When someone visits /api/jobs/search?query=java+developer&location=Lansing+Michigan&page=1, Spring Boot automatically extracts the values and passes them to your method parameters:
querygets the value"java developer"locationgets the value"Lansing Michigan"pagegets the value1
Notice that page has defaultValue = "1". This means if the user does not include a page parameter in the URL, it defaults to 1. The query and location parameters are required — if someone calls the endpoint without them, Spring Boot will return a 400 Bad Request error automatically.
The method body is beautifully simple: it calls jobSearchService.searchJobs() with the parameters and returns whatever comes back. The service handles all the complexity of building the URL, setting headers, and making the API call. The controller just passes the request along and returns the result. This clean separation is exactly how professional Spring Boot applications are structured.
Part 5 — Testing the API
It is time to see this all in action. Make sure your application.properties file has your RapidAPI key and host configured, then start your Spring Boot application using ./mvnw spring-boot:run (or run the main class from your IDE). Wait for the startup message confirming the server is running on port 8080.
Testing in the Browser
Open your web browser and navigate to:
http://localhost:8080/api/jobs/search?query=java+developer&location=Lansing+Michigan
After a moment (the JSearch API needs a second or two to respond), you should see a large block of JSON appear in your browser. This is real job listing data. Let us look at what comes back.
Understanding the JSON Response
The JSearch API returns a JSON object with several fields. The most important one is data, which contains an array of job listings. Each job listing is a JSON object with many fields. Here are the most useful ones for our Resumator project:
{
"status": "OK",
"data": [
{
"job_id": "abc123",
"job_title": "Junior Java Developer",
"employer_name": "Acme Corporation",
"employer_logo": "https://example.com/logo.png",
"job_city": "Lansing",
"job_state": "MI",
"job_description": "We are looking for a motivated Junior Java Developer...",
"job_apply_link": "https://acme.com/careers/java-dev",
"job_min_salary": 55000,
"job_max_salary": 75000,
"job_salary_currency": "USD",
"job_salary_period": "YEAR"
},
{
"job_id": "def456",
"job_title": "Java Backend Engineer",
"employer_name": "TechStart LLC",
"employer_logo": null,
"job_city": "East Lansing",
"job_state": "MI",
"job_description": "Join our growing engineering team...",
"job_apply_link": "https://techstart.io/apply/java",
"job_min_salary": null,
"job_max_salary": null,
"job_salary_currency": null,
"job_salary_period": null
}
]
}
Let us go through the key fields:
job_title— The title of the position, like "Junior Java Developer" or "Java Backend Engineer."employer_name— The company that is hiring.employer_logo— A URL pointing to the company's logo image. This can benullif no logo is available.job_cityandjob_state— The location of the job.job_description— A full description of the role, responsibilities, and requirements. This is often a long string that may contain HTML formatting.job_apply_link— A direct URL where the user can apply for the job. This is crucial for Resumator — we want users to be able to click through and actually apply.job_min_salaryandjob_max_salary— The salary range, if provided. Many listings do not include salary information, in which case these fields will benull.job_salary_currencyandjob_salary_period— The currency (usually "USD") and pay period (usually "YEAR" for salaried positions or "HOUR" for hourly positions).
These are real jobs. If you click on a job_apply_link value, it will take you to an actual job application page. The data comes from real company career pages that are actively hiring. This is what makes the Resumator project so practical — you are building a tool with real, useful data.
Try It with a Fetch Call
You can also test the endpoint using JavaScript fetch(), just like you did in Lesson 13. The editor below shows how to call your search endpoint. If your Spring Boot server is running, try modifying the query and location to search for different jobs:
// This would work if your Spring Boot server is running
// Try modifying the query and location!
const query = "java developer";
const location = "Lansing Michigan";
const url = `/api/jobs/search?query=${encodeURIComponent(query)}&location=${encodeURIComponent(location)}`;
console.log("Fetching: " + url);
console.log("(This is a demo — your Spring Boot server needs to be running for real results)");
Notice the use of encodeURIComponent(). This function converts special characters (like spaces) into their URL-safe equivalents. For example, "java developer" becomes "java%20developer." This ensures the URL is valid even when the search terms contain spaces or special characters. Always encode user input when putting it into URLs — this is a habit that will prevent bugs and security issues.
Part 6 — Error Handling
So far, we have assumed everything goes perfectly: the API responds quickly, the response is valid, and nothing goes wrong. In the real world, things go wrong all the time. The JSearch API might be temporarily down for maintenance. Your API key might expire. The user might enter a search query that returns no results. Your internet connection might drop in the middle of a request. A professional application needs to handle all of these scenarios gracefully instead of crashing.
Let us update our JobSearchService to include proper error handling. Here is the improved version of the searchJobs method:
public String searchJobs(String query, String location, int page) {
try {
String url = "https://jsearch.p.rapidapi.com/search"
+ "?query=" + query + " in " + location
+ "&page=" + page
+ "&num_pages=1";
HttpHeaders headers = new HttpHeaders();
headers.set("X-RapidAPI-Key", apiKey);
headers.set("X-RapidAPI-Host", apiHost);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
String.class
);
return response.getBody();
} catch (Exception e) {
// Log the error so we can debug it later
System.err.println("Error calling JSearch API: " + e.getMessage());
// Return a JSON error message instead of crashing
return "{\"status\": \"ERROR\", \"message\": \"Failed to fetch job listings. Please try again later.\", \"data\": []}";
}
}
The key change is wrapping the entire API call in a try/catch block. If anything goes wrong inside the try block — a network timeout, an invalid response, a server error from JSearch — the code jumps to the catch block instead of crashing your entire application.
Inside the catch block, we do two things:
- Log the error. We print the error message to the console using
System.err.println(). This is important for debugging — if something goes wrong in production, you can look at the logs to figure out what happened. In a real application, you would use a proper logging framework like SLF4J, butSystem.errworks fine for learning. - Return a structured error response. Instead of letting the exception bubble up and showing the user an ugly error page, we return a JSON string that has the same general structure as a successful response but with an error status and an empty data array. This way, the frontend can check the
statusfield and show a user-friendly message like "No results found. Please try again later." without breaking.
Rate Limiting
Most third-party APIs have rate limits — a maximum number of requests you can make within a certain time period. The free tier of JSearch on RapidAPI typically allows a limited number of requests per month. If you exceed this limit, the API will respond with a 429 Too Many Requests error.
You do not need to worry about rate limits during development, since you will only be making a few requests at a time. But it is good to be aware that rate limits exist. In a production application, you would implement caching (storing API responses temporarily so you do not repeat identical requests) and rate limit monitoring (tracking how many requests you have made and slowing down if you are approaching the limit).
A good rule of thumb for error handling: never show your users raw error messages or stack traces. These are useful for developers but confusing and potentially scary for users. Always catch errors and return a clean, understandable response that the frontend can display gracefully.
Part 7 — The Complete Data Flow
Now that all the pieces are built, let us step back and look at the complete picture. When a user searches for jobs in the Resumator, here is exactly what happens, step by step:
Let us trace a complete request through this chain:
- The user types a search. They enter "java developer" in the search field and "Lansing Michigan" as the location, then click "Search." The browser builds a URL like
/api/jobs/search?query=java+developer&location=Lansing+Michiganand sends an HTTP GET request. - JobSearchController receives the request. Spring Boot routes the request to the
searchJobs()method based on the@GetMapping("/search")annotation. The@RequestParamannotations extract the query and location values from the URL. - The controller delegates to JobSearchService. The controller calls
jobSearchService.searchJobs("java developer", "Lansing Michigan", 1). It does not know or care how the service gets the data — it just passes the parameters along. - JobSearchService calls the JSearch API. The service builds the full JSearch URL, sets the authentication headers with your API key, and uses
RestTemplateto send an HTTP GET request tohttps://jsearch.p.rapidapi.com/search. - JSearch processes the request. The JSearch API searches its database of job listings, finds matches for "java developer" near "Lansing Michigan," and builds a JSON response containing the results.
- The response travels back. JSearch sends the JSON response back to your
RestTemplate. The service extracts the response body and returns it to the controller. The controller returns it to Spring Boot, which sends it as an HTTP response to the browser. - The browser displays the results. The browser receives the JSON, your frontend JavaScript parses it, and the job listings appear on the screen. The entire round trip typically takes 1–3 seconds, most of which is spent waiting for JSearch to process the search.
This is the same pattern used by every application that integrates with a third-party API. The details change — different URLs, different headers, different response formats — but the architecture is always the same: Browser → Your Controller → Your Service → External API, and then the response flows back through the same chain in reverse.
Why Not Call JSearch Directly from the Browser?
You might wonder: "Why do we need the Spring Boot server in the middle? Why not have the browser call the JSearch API directly?" There are two critical reasons:
- Security. Calling JSearch requires your API key. If you put that key in your frontend JavaScript code, anyone who visits your website could open the browser's developer tools, find your key, and use it for their own purposes. By keeping the API key on your server (in
application.properties), it never gets sent to the browser. The browser only talks to your server, and your server handles the sensitive API call. - Control. Having the request pass through your server lets you add logging, caching, rate limiting, input validation, and data transformation. You are in control of what your users can search for and how the data is formatted before it reaches them.
Quiz — Check Your Understanding
1. What is the purpose of the @Service annotation in Spring Boot?
Correct answer: B. The @Service annotation tells Spring Boot that a class contains business logic and should be managed by the Spring container. Spring Boot creates an instance of the class automatically and makes it available for dependency injection into other classes (like controllers). It does not handle HTTP requests directly — that is the job of @RestController.
2. Why do we store the API key in application.properties instead of hardcoding it in the Java file?
Correct answer: C. Storing sensitive values like API keys in application.properties (and adding that file to .gitignore) keeps them separate from your source code. This makes it easy to change keys without modifying Java code, prevents accidental exposure in public repositories, and allows different environments (development, staging, production) to use different keys without code changes.
3. In the data flow diagram (Browser → Controller → Service → JSearch API), why does the browser not call the JSearch API directly?
Correct answer: C. If the browser called JSearch directly, the API key would have to be included in the frontend JavaScript code, where anyone could see it using the browser's developer tools. By routing the request through your Spring Boot server, the API key stays safely on the server and is never sent to the browser. This server-side proxy pattern also gives you the ability to add caching, logging, and rate limiting.
Lesson Summary
This lesson was a major step forward. You went from building self-contained APIs (like the contact form in Lesson 13) to building an API that communicates with the outside world. Let us recap everything you accomplished:
- Third-party API integration. You learned what third-party APIs are, why they are preferable to web scraping, and how to sign up for and authenticate with the JSearch API through RapidAPI.
- Service class architecture. You built a
JobSearchServiceclass that separates business logic (calling JSearch) from request handling (the controller). This is a professional best practice that keeps your code organized and maintainable. - RestTemplate. You learned how to use Spring's
RestTemplateto make HTTP requests from your server to another server, including setting custom headers for authentication. - Property injection. You stored your API key in
application.propertiesand used@Valueto inject it into your service class, keeping sensitive credentials out of your source code. - Controller with @RequestParam. You built a
JobSearchControllerwith@RequestMappingfor a base path and@RequestParamfor extracting search parameters from the URL. - Error handling. You added try/catch blocks to handle API failures gracefully, ensuring your application returns a clean error response instead of crashing.
- Data flow understanding. You traced the complete journey of a request from the browser through your controller, through your service, to the JSearch API, and all the way back.
You now have a working backend that can search for real job listings. But right now, the only way to see the results is by typing a URL into your browser's address bar and reading raw JSON. That is not exactly a pleasant user experience.
In the next lesson, we are going to fix that. You will build a beautiful search interface with HTML, CSS, and JavaScript that lets users type in a job title and location, click a search button, and see the results displayed as formatted job cards. The backend you just built will power that frontend — and your Resumator will start to feel like a real application.
Finished this lesson?