DEV Community

Cover image for CustomsBuddy: Your customs broker for international parcels
πn
πn

Posted on

CustomsBuddy: Your customs broker for international parcels

This is a submission for the Built with Google Gemini: Writing Challenge

What I Built with Google Gemini

Sending international packages is a pain. Especially when it comes to filling out customs declarations (CN22/CN23).

The average user doesn't know what "HS Codes" are, doesn't know how to correctly translate the names of specific items into English customs language, and often doesn't even realize that their favorite homemade cookies or cold pills are prohibited from being imported into the destination country.

To solve this problem, I created CustomsBuddy — a React application that converts a user's chaotic text description of a package into a rigorous, validated array of data.

In this article, I'll show how we abandoned unreliable text parsing and used Structured Output (JSON Schema) and dynamic System Instructions in the Gemini API.

Demo:

🛑 Why don't regular prompts work for UI?

Let's imagine a typical input: "I'm sending two used t-shirts, a new book, homemade cookies, and 50 bucks worth of medicine to Canada."

If you ask a regular LLM to "make a table out of this," it will return Markdown. Markdown looks great in chat, but as a developer, I need to draw React components: green checkmarks for allowed items, red warnings for prohibited items, and calculate the total.

I need predictable JSON.

🛠 Solution: Gemini Structured Output

Instead of begging the model to "please return only JSON," we use the responseSchema parameter in the Gemini API. This forces the model to strictly follow the structure we specify.

Step 1: Define the Ideal Structure (The Schema)

Here is the configuration object that we will pass to the API:

const responseSchema = {
  type: "OBJECT",
  properties: {
    items: {
      type: "ARRAY",
      items: {
        type: "OBJECT",
        properties: {
          original_description: { type: "STRING" },
          customs_description_en: { type: "STRING" },
          hs_code: { type: "STRING" }, // International customs code
          quantity: { type: "INTEGER" },
          weight_kg: { type: "NUMBER" },
          value_usd: { type: "NUMBER" },
          is_allowed: { type: "BOOLEAN" }, // The main trigger for UI!
          warning_reason: { type: "STRING" } // Explanation if prohibited
        },
        required: ["original_description", "customs_description_en", "hs_code", "quantity", "weight_kg", "value_usd", "is_allowed", "warning_reason"]
      }
    },
    general_warnings: {
      type: "ARRAY",
      items: { type: "STRING" }
    }
  },
  required: ["items", "general_warnings"]
};
Enter fullscreen mode Exit fullscreen mode

Step 2: Dynamic System Instructions

Our app supports different destination countries (USA, Canada, Germany, etc.) and different interface languages. The US customs rules are different from Germany. That's why we make the systemInstruction dynamic!

In the React component, before calling the API, we form the instruction:

// Get the country name in English and the language for the explanation
const destCountryEn = countries.find(c => c.code === destination).name.en;
const explanationLang = lang === 'uk' ? 'українською мовою' : 'in English';

const systemInstruction = `You are a professional international customs broker.
Your task: to analyze user input to send parcels to the country: ${destCountryEn}.
1. Translate the product descriptions into strict, standardized English.
2. Determine the exact 6-digit HS Code.
3. Logically distribute the cost/weight if only the total is specified.
4. STRONG VALIDATION: Check the customs rules of the country ${destCountryEn}. If the item is prohibited (e.g., homemade food, certain medications), set is_allowed: false and explain why, ${explanationLang} is mandatory. Respond strictly in JSON format.`;
Enter fullscreen mode Exit fullscreen mode

Step 3: Calling the Gemini API

Now we send all this to gemini-2.5-flash:

const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  contents: [{ parts: [{ text: inputText }] }],
  systemInstruction: { parts: [{ text: systemInstruction }] },
  generationConfig: {
    responseMimeType: "application/json",
    responseSchema: responseSchema // Inserting our schema!
  }
 })
});

const data = await response.json();
const jsonText = data.candidates[0].content.parts[0].text;
const parsedDeclaration = JSON.parse(jsonText);
// That's it! No regular expressions or text truncation!
Enter fullscreen mode Exit fullscreen mode

🎯 How does this look in practice?

When a user types: "I'm sending two T-shirts, a book, homemade cookies, and $50 worth of cold medicine to Canada"...

Gemini returns a perfect JSON where for the book and T-shirts is_allowed: true (and the model itself found the HS codes 4901.99 and 6309.00 for them). But for the cookies and medicine, the model returns is_allowed: false and adds an explanation that Canadian customs blocks homemade food without factory labeling and over-the-counter medicines.

In React, we simply map this array:

{results.items.map((item, idx) => ( 
 <div key={idx} className={item.is_allowed ? 'bg-slate-50' : 'bg-red-50'}> 
  {item.is_allowed ? <CheckCircle2 className="text-emerald-500" /> : <AlertOctagon className="text-red-500" />} 

  <h3>{item.customs_description_en}</h3> 
  <span>HS Code: {item.hs_code}</span> 

  {!item.is_allowed && ( 
  <div className="text-red-700"> 
   Failure reason: {item.warning_reason} 
  </div> 
  )} 
 </div>
))}
Enter fullscreen mode Exit fullscreen mode

💡 Conclusion

Structured Output (JSON Schema) completely changes the approach to developing AI applications. We no longer use LLMs simply as "smart interlocutors". We use them as reliable microservices for data analysis, classification and transformation that integrate perfectly into our frontend.

Try adding responseSchema to your next Gemini project - and you will forget about the pain of text parsing forever!

Top comments (0)