Building an API that works on your local machine is just the first step. But what separates a hobby project from a production-ready product? In today's post, I'll show how I transformed my feedback system into a robust, secure, and fully documented application.
π 1. The Digital "Key": Implementing JWT
Up until now, anyone who discovered my API URL could read every feedback ever submitted. In a real-world scenario, this is a critical privacy failure.
To solve this, I implemented JWT (JSON Web Token) using @fastify/jwt. The flow now works as follows:
- The admin logs in with secure credentials.
- The API generates a signed token.
- This token must be sent in the header of every request to protected routes.
The game-changer here was using a Fastify decorator to create an authenticate hook. Now, protecting a route is as simple as adding a single line of code.
π 2. Pro Swagger: Documentation that actually works
Documentation isn't enough; it needs to be useful. I was already using Swagger, but the challenge was: How do I test protected routes without leaving the browser?
I configured securitySchemes in Swagger to support the Bearer Auth pattern. The result? An "Authorize" button with a lock icon appeared at the top of the page. Now, I can log in through the interface itself, paste the token, and test private endpoints directly. Pure productivity.
π‘οΈ 3. Environment Shielding with Zod
A common mistake is an application failing in production because someone forgot to set a variable in the .env file.
To prevent this, I used Zod to create an environment "validator." I built a schema that checks:
- If
JWT_SECRETexists and is secure. - If
DATABASE_URLis correct. - If
PORTis actually a number.
If any of these variables are missing or wrong, the application crashes immediately at startup and tells you exactly what's wrong. This Fail-fast approach is a senior-level concept that brings real peace of mind.
// Validating our environment variables with Zod
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
PORT: z.coerce.number().default(3000),
JWT_SECRET: z.string().min(1, "JWT_SECRET is required"),
DATABASE_URL: z.string().min(1, "DATABASE_URL is required"),
});
const _env = envSchema.safeParse(process.env);
if (_env.success === false) {
console.error('β Invalid environment variables!', _env.error.format());
process.exit(1); // Stop the app if config is wrong
}
ποΈ 4. Refactoring and Clean Architecture
As the API grew, the main file (app.js) started getting cluttered. I applied the "Separation of Concerns" principle:
Routes: Specific files to define paths and schemas.
Controllers: The bridge between HTTP and business logic.
App: Only orchestration and plugin registration.
This organization allowed the project to breathe and made maintenance much easier.
π Pro-tip: Handling Git and .env
During the process, I learned (the hard way!) that adding .env to .gitignore after it's already tracked doesn't work. I had to clear the Git cache (git rm --cached .env) to ensure my secrets weren't stored in the commit history. Pro-tip: security is never too much!
Conclusion
My feedback API is no longer just code that saves data; it's an ecosystem with Testing (Vitest), Security (JWT), Interactive Docs (Swagger), and Continuous Deployment (Render).
What tools are you using to secure your APIs? Letβs talk in the comments! π
Top comments (0)