July 19, 2026
Build N’ Break: API Lab
Recently, I built an API using Node and express. Recently, I built an API using Node.js and Express. Building an API isn’t particularly…

By Sam Hilliard
4 min read
Recently, I built an API using Node and express. Recently, I built an API using Node.js and Express. Building an API isn't particularly novel, but that wasn't my goal. Instead, I wanted to build an application I could be proud of from a security perspective.
I wanted to make it as secure as possible — from design to the automation tests. Then, I'm going to purposely introduce different types of vulnerabilities to learn what development mistakes cause them to happen in the wild.
Through working on this project, I plan to strengthen my secure code review skills, mitigation techniques, and get a better understanding of vulnerability root causes.
Tech Stack
I chose Node.js with Express and TypeScript for the backend. Not only because they're popular technologies, but because I use them at my day job and want to get more familiar with them from a security standpoint.
TypeScript can help reduce certain classes of bugs by making data types explicit and catching type-related mistakes during development. While it doesn't eliminate security vulnerabilities, it encourages safer data handling patterns when used correctly.
For my database, I chose PostgreSQL (for simplicity and popularity). I also Dockerized my app for ease of setup and because that's a technology worth knowing well.
Architecture
I kept things pretty simple for this application. I used JWTs for authentication. The assets in this application are users, organizations, and documents. Organizations contain users, while documents represent organization-owned intellectual property.
Pretty basic but you'd be surprised at the complexity development and testing took. I can see how easy it is to introduce mistakes regarding authorization — especially as applications grow in complexity.
If you're interested, I outline threats here.
Security Best Practices
Now, I'll dive into how I applied best practices to make this application secure. By no means am I saying it's bulletproof. However, I did take several general steps that helped me bake-in secure coding best practices and prevent abuse.
Strict Typing and Linting
TypeScript reduces the likelihood of unexpected data handling by enforcing compile-time type checks. It prevents unexpected data handling through syntactic typing. You know for example, that a variable you're handling is an integer and can treat it as such.
But Typescript is only effective at preventing inconsistent data handling if it is used correctly. Enter linting and strict typing.
I used ESLint combined with strict typing to enforce best practices and correct use of typing. If, for example, data was typed as any (which overrides typing), a build error would be thrown. This prevents developers from implementing bad or insecure patterns.
While it won't guarantee perfect code, it's a layer of defense that can help coerce the writing of more secure code.
Input validation with Zod
It's best practice to not accept any input that doesn't make syntactic or semantic sense in the context of your app. Zod makes it easy to consistently do this throughout your application.
Plus, I find the schema structure integrates well with Typescript. For example, you can make a custom schema, then export it as a type.
Test coverage
In my opinion, tests are the most important aspect of a project. Having both unit tests (granular tests that run in isolation) and integration tests (end-to-end tests on a running instance) that cover 100% of your code base is vital.
Tests should of course test the functionality of your code to prevent new changes from causing new bugs (regressions). But, security tests should be written as well.
For example, I added unit tests to ensure that my API correctly validated input. On the other hand, integration tests can be used to validate authentication and authorization.
Automated security tests can detect application-specific vulnerabilities that static (SAST) and dynamic (DAST) scanners often miss because those tools lack knowledge of your application's intended behavior.
Husky pre-commit hooks
Husky enables you to run scripts before every commit.
My pre-commit hooks run linting, formatting, type checking, and my tests to ensure that code is consistent and does not introduce any regressions before being checked in.
Besides being a preventative measure, Husky hooks also add a level of convenience. It's nice to not have to run each script individually. If you had to run them all manually, it's likely developers would skip running these scripts.
Rate Limiting
When implemented correctly, rate limiting can help protect your infrastructure from denial of service attacks and help thwart brute-forcing attempts.
It's best not to try to re-invent the wheel and use an existing library. Especially when it comes to security controls. I used Express Rate Limit in my project because it is well-known, currently maintained, and fit my specific usecase.
Centralized Logging
Logging is not just handy for debugging issues. It's vital in security. Structured, centralized logs make it significantly easier to search, correlate, and analyze security events.
When a security incident occurs, responders need an audit trail to follow complete with timestamps and as much context as possible without leaking PII. You can also protect users by notifying them of attacks and providing them with preventative steps (like changing their passwords).
In my project, I used Morgan to easily log HTTP requests.
Dependency Scanning
No matter how secure your app is, that means almost nothing if you have vulnerable dependencies. It's almost impossible to manually check every dependency in your app against known vulnerabilities.
Enter dependency scanning. npm provides npm audit, which checks dependencies against known vulnerabilities. But, it's best to have a cross-verification with at least two dependency scanners. I use OWASP Dependency Check as my second dependency scanner to backup npm.
Documentation
Last, and certainly not least, it's important to keep documentation up to date. This way, developers and testers can collaborate effectively on the same project. In the world of AI, it also provides agents with more context so they can make better, more efficient decisions.
In my project, I included OpenAPI specification to make my API's structure transparent. Tools like Postman can ingest OpenAPI spec. to make it easy to test the API end-end.
Conclusion
I learned a lot building this application with a security-first mindset. There are a lot of simple, yet powerful security best practices you can follow to greatly improve the security of your application and shift security left.
It's best to implement as many complementary security controls as practical. Just having security test coverage, for example, wouldn't protect your app against brute-force attacks with the same effectiveness as rate limiting.
In the future, I'll add intentional vulnerabilities to experiment with how developer mistakes can lead to security vulnerabilities.