I've been building small APIs lately, mostly as a way to learn by actually shipping things instead of just reading about them.
One of the projects I ended up spending more time on than I expected was a QR Code API.
At first, the idea was pretty simple:
Send some data → get a QR code.
That should be straightforward, right?
Well, it was, until I started thinking about all the things I wanted to customize.
What if I want a gradient instead of a single color?
What if I want the corners to look different from the rest of the QR code?
What if I want SVG instead of PNG?
What if I need to decode a QR code somewhere in my application later?
So I kept adding things, and the project gradually turned into more than just a basic QR generator.
I recently published it on RapidAPI, so I wanted to share what I built and a little bit of what I learned along the way.
What does it do?
The API supports both QR code generation and QR code decoding/validation.
For generation, you can customize several parts of the QR code, including:
- PNG, JPEG, WebP, and SVG output
- Custom gradients
- Separate gradients for the QR body, corner squares, and corner dots
- Different body/module shapes
- Different dot styles
- URL or Base64 input
- Different response types depending on the endpoint
You can also use structured QR data such as Wi-Fi, email, telephone, SMS, geolocation, and contact information through the appropriate request fields.
The goal wasn't really to build the biggest QR platform possible.
I just wanted something flexible enough that I could use it in projects where a basic black-and-white QR code wasn't quite enough.
A simple Rust example
Since the API itself is written in Rust, I thought it would be more useful to show the examples in Rust too.
The API is just a REST API, so of course you can call it from other languages as well.
Here's a minimal request using reqwest:
use reqwest::Client;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let response = client
.post("https://qr-code125.p.rapidapi.com/qr")
.header("X-RapidAPI-Key", "YOUR_RAPIDAPI_KEY")
.json(&json!({
"data": "https://example.com",
"format": "png",
"size": 512,
"response_type": "base64"
}))
.send()
.await?;
let body = response.text().await?;
println!("{}", body);
Ok(())
}
Cargo.toml:
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.13", features = ["json"] }
serde_json = "1"
It returns base64 output. And if you decode that, you will get like this QR:
The basic request is intentionally small. Most of the other parameters are optional customization.
You can also use RapidAPI's Copy Code feature from the playground to get the request in other languages.
Customization
This is where the project became much more interesting for me.
Generating a normal QR code isn't particularly complicated.
The fun part was figuring out how much of the visual appearance I could expose without making the API completely confusing.
For example, the QR body and the corner elements can have separate gradients.
Conceptually:
QR body
└── gradient A
Corner squares
└── gradient B
Corner dots
└── gradient C
You can also change the shape of the QR modules and dots.
That means the same data can produce very different-looking QR codes.
I initially thought the styling part would be relatively small.
It wasn't.
Once you combine different shapes, gradients, corner styles, output formats, and error-correction settings, the number of possible combinations grows pretty quickly.
That's probably one of the more interesting parts of building something like this: the normal case is easy, but edge cases tend to show up everywhere.
A more customized request
Here's a more complete example:
use reqwest::Client;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let response = client
.post("https://qr-code125.p.rapidapi.com/qr")
.header("X-RapidAPI-Key", "YOUR_RAPIDAPI_KEY")
.json(&json!({
"format": "png", // svg, png, jpeg, webp
"size": 512, // 512 ~ 4096
"margin": 3, // 0 ~ 10
"error_correction": "H", // L, M, Q, H
"style": "extra_rounded", // square, dots, rounded, classy,
// classy_rounded, extra_rounded
"shape": "square", // square, circle
// Use either `data` or `type` + `payload`.
"data": "Hello world",
// Example:
// "type": "sms",
// "payload": {
// "phone": "+821012345678",
// "message": "hello"
// },
"foreground_color": "#000000",
"background_color": "#FFFFFF",
"gradient_type": "linear", // linear, radial
"gradient_direction": 0,
"gradient_start": "#7DD3FC",
"gradient_end": "#3B82F6",
"corner_dot": "dot",
"corner_dot_gradient_type": "linear",
"corner_dot_gradient_direction": 0,
"corner_dot_gradient_start": "#7DD3FC",
"corner_dot_gradient_end": "#3B82F6",
// "corner_dot_color": "#3B82F6",
"corner_square": "dot",
// "corner_square_gradient_type": "linear",
// "corner_square_gradient_direction": 0,
// "corner_square_gradient_start": "#7DD3FC",
// "corner_square_gradient_end": "#3B82F6",
"corner_square_color": "#7DD3FC",
"response_type": "base64" // json, base64, direct
}))
.send()
.await?;
let body = response.text().await?;
println!("{}", body);
Ok(())
}
It returns base64 output. And if you decode that, you will get like this QR:
One thing to keep in mind is that some options are endpoint-specific, so the API playground is still the best place to check the currently available parameters.
Output formats
The API supports several output formats:
PNG
JPEG
WebP
SVG
SVG is useful when you want the QR code to scale without losing quality.
WebP can be useful for web applications where file size matters.
I wanted to keep the output format flexible instead of forcing every use case into the same image format.
Decoding QR codes
At some point while building the generator, I realized that generating QR codes wasn't the only thing I wanted the API to do.
There are also cases where an application receives a QR image and needs to figure out what's inside it.
So I added decoding and validation endpoints as well.
The general workflow looks like this:
QR image
↓
Upload / provide image data
↓
Decode
↓
Extract QR contents
↓
Validate
This can be useful for applications where users upload QR images and your backend needs to inspect them automatically.
URL and Base64 input
Another small thing I wanted to support was different ways of passing image data.
Depending on the endpoint, you can work with a URL or Base64-encoded data.
That means you don't always have to save an image somewhere first just to send it to the API.
For backend applications, that can make the integration a little simpler.
Why Rust?
I mainly chose Rust because I wanted to build something real with it, rather than another small toy project.
Once image processing, API handling, and error cases started coming together, it turned out to be a pretty good project for learning.
I ended up working on things like:
- HTTP API design
- Image processing
- Encoding and decoding
- Request handling
- Error handling
- API deployment
The original idea was small, but it kept growing as I found more things I wanted to support.
That's probably my favorite part of building side projects: the final project rarely looks exactly like the thing you originally imagined.
What I learned
One thing I learned from this project is that adding features is easy compared to deciding which features actually matter.
It's surprisingly easy to keep adding another option:
"Maybe someone will need this."
And then another one.
And another one.
At some point you have a lot of flexibility, but also a much more complicated API.
So one thing I want to improve next is the balance between flexibility and simplicity.
I'd also like to add more examples for different languages and make the documentation easier to navigate.
Who is this API for?
I think there are a few situations where an API like this can be useful:
- Generating QR codes dynamically in a web application
- Creating QR codes from user input
- Generating QR assets for emails, documents, or product pages
- Adding QR functionality to an existing SaaS
- Decoding QR images uploaded by users
- Validating QR-related input on the backend
The idea is simply to handle the QR generation or decoding through an API instead of implementing that functionality from scratch inside the application.
Try it
I've published the API on RapidAPI:
https://rapidapi.com/haruato12/api/qr-code125
You can try the endpoints directly from the playground and experiment with the available parameters.
I'm still working on the project, so I'm particularly interested in feedback from other developers.
For example:
- Was the API easy to understand?
- Are there parameters that feel confusing?
- Is there a QR-related feature that would make it more useful?
I'm trying to avoid adding features just for the sake of adding them, so real-world feedback would be much more useful.
Thanks for reading!


Top comments (0)