Self-hosted service
edwinhern/express-typescript avatar
edwinhern/express-typescript

express-typescript: an Express 5 backend template with the decisions already made

Express + TypeScript starter template

1,247 stars243 forksTypeScriptMIT

At a glance

What is it?
express-typescript is a MIT licensed starter template for Express 5 and TypeScript that arrives with Zod validation, generated Swagger documentation, pino logging, Helmet, a rate limiter, Vitest with Supertest, Biome for linting and a multi-stage Docker image. It saves you from choosing a stack, and it costs you a folder layout you did not design.
Who is it for?
express-typescript fits a team starting a JSON API that would otherwise spend a week choosing a validation library, a logging library, a test runner and a lint configuration, and that is happy to delete the sample health check and user module and keep the plumbing. It does not fit a team with an existing house style, a GraphQL or server-rendered application, or a service that needs to run on an old Node version.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The value is in the choices you would otherwise make badly

express-typescript is a template for building backend web services with Express.js and TypeScript, and the README describes it plainly as a simple and ready-to-use starting point. The listed benefits are starting new projects faster, writing clean and consistent code, building things quickly, and following best practices for security and testing.

The interesting part is what that buys you in practice. The dependency list is a considered set rather than a default one. Express is at 5.2.1, and the validation story is Zod at 3.25.76, which is then also used to generate the OpenAPI document through @asteasolutions/zod-to-openapi, so the schema you validate against is the schema the documentation is written from. Swagger UI is served with swagger-ui-express. Security headers come from Helmet and CORS is configured, and request rate limiting uses express-rate-limit.

Logging is pino with pino-http for the request logs, and pino-pretty is in the dev dependencies for local reading. Tests are Vitest with Supertest, and coverage runs through @vitest/coverage-v8. Style and linting are handled by Biome, and TypeScript is 5.9.2 with tsup doing the bundling.

A template like this is only worth adopting if you agree with its choices, because the whole point is that you do not have to make them. A team that has already decided on a different validation library or a different logger will spend the first afternoon undoing this, and a team that has not will save a week.

The folder layout is the architecture, and it is opinionated

The structure the README prints is the clearest description of what the template thinks a backend looks like, and it is worth reading line by line rather than as a tree to skim.

The top of src/ holds index.ts and server.ts, so the entry point and the server construction are separate, which is what lets a test import the app without binding a port. Under src/api/ the code is grouped by feature, with healthCheck/ and user/ as the two examples. The user feature is the interesting one, because it is split into a router, a controller, a service, a model and a repository, each with its own file, with tests for the router and the service in a __tests__ directory.

That five-file split for one entity is a strong opinion about layering, and it is a common one in the Java and.NET world more than in Node, where a single route file is the default. It is also the part of the template you are most likely to keep, since it is where the dependency direction lives: the router talks to the controller, the controller to the service, the service to the repository.

The shared pieces live in src/common/. The middleware folder holds errorHandler, rateLimiter and requestLogger. The models folder holds serviceResponse.ts, which is the unified API response type the README advertises, and the utils folder holds commonValidation, envConfig and httpHandlers.

There is also src/api-docs/ with openAPIDocumentGenerator.ts, openAPIResponseBuilders.ts and openAPIRouter.ts, which is the machinery that turns the Zod schemas into the served documentation. The naming is consistent enough to be learnable, which is the point of a template.

Getting it running is three steps

The setup is short enough to reproduce from the README without guessing.

Clone the repository, move into the directory and install the dependencies.

bash
git clone https://github.com/edwinhern/express-typescript.git
cd express-typescript
pnpm install

Then create your environment file by copying the template, .env.template to .env, and fill in the variables it asks for.

The README says the environment settings are checked with Zod to prevent errors, and envConfig.ts is where that happens. The practical consequence is worth understanding: a missing or malformed variable is a startup failure with a clear message rather than an undefined value that surfaces hours later in a request. That is a genuinely good default, and it also means your deployment environment has to set every variable the template expects, which is the first thing that trips people up when they containerise it.

The third step is running it. Development mode uses tsx to run TypeScript directly with watch, and there is a separate build step.

bash
pnpm start:dev
pnpm build
pnpm start:prod

The README also says to set NODE_ENV to production in the .env file before running the production command, so the environment file carries more than secrets. There is a video demo of the setup and the running project linked from the README for people who would rather watch than read.

Tests, linting and a build that emits two module formats

The package scripts describe the whole inner loop, and they are short.

json
"build": "tsc && tsup",
"start:dev": "node --import=tsx --watch src/index.ts",
"start:prod": "node dist/index.js",
"check": "biome check --write",
"test": "vitest run",
"test:cov": "vitest run --coverage"

The test script runs vitest run, and the coverage variant adds the coverage flag. The check script is biome check --write, which both lints and formats in one pass, so the formatter and the linter cannot disagree the way separate tools eventually do.

The build is tsc and tsup, and the tsup configuration in package.json is where the packaging decisions are recorded. The entry point is src/index.ts, the output directory is dist, and the format list is both esm and cjs, so the published output works in a project using import and in one using require. The target is es2020, source maps are on, and dts is on, which means the type declarations are generated as well.

The test setup is worth a note because it is more complete than most templates. Supertest is there for HTTP level testing, and the __tests__ directories in the tree are not empty decoration, since the user feature has a router test and a service test, the health check has a router test, the error handler and the request logger each have a test, and the API docs router has one.

The result is a template where the example code is tested. That is the difference between a starter kit that shows you how the pieces fit and one that shows you how they are supposed to be tested.

The Dockerfile is a real multi-stage build, with one sharp edge

The Dockerfile is more careful than most, and it is worth reading as a template in its own right.

The base stage starts from node:23.11.1-slim, sets PNPM_HOME, puts it on the PATH and runs corepack enable, so pnpm is the package manager inside the image and the version comes from the packageManager field in package.json rather than from a global install. There are then three stages. The prod-deps stage copies only package.json and pnpm-lock.yaml and installs production dependencies with pnpm install --prod --frozen-lockfile --ignore-scripts, using a BuildKit cache mount on the pnpm store. The build stage installs everything including dev dependencies, copies the source and runs pnpm run build. The final stage is based on node:23.11.1-alpine, copies the production node_modules and the dist directory across, switches to the node user, exposes port 8080 and starts the built entry point with node dist/index.js.

The sharp edge is --ignore-scripts. It is a deliberate supply chain decision, and it is the right default, but a dependency whose postinstall step compiles a native module will silently end up without that module. If you add such a dependency, you need to handle its install step explicitly rather than removing the flag on a hunch.

The other detail is the port. The image exposes 8080, so the application is expected to read its port from the environment, and the .env file and the container configuration have to agree. The application is also a private package, so the template is not published to a registry as a reusable library, it is a repository you copy.

A template that is maintained by pushes, not by releases

The version and release history tell an interesting story about how this project is run.

The package version is 1.0.14, and the published releases stop at v1.0.14 on 2024-07-03, with v1.0.13 and v1.0.12 in the weeks before it. The repository, however, was last pushed to on 2026-10-15, so the template itself has moved a long way past the last tagged release, with the dependency versions in package.json telling the same story. Express 5.2.1, TypeScript 5.9.2, Biome 2.4.9 and Vitest 3.2.4 are not the versions that shipped in the 1.0.14 release.

The practical meaning is that you should copy the repository rather than fetch the last release tag, because the tag is two years behind the working template.

There is also a naming history in the metadata. The package repository field points at edwinhern/express-typescript-2024, and the CI badge in the README points at the same repository, while the project itself is now edwinhern/express-typescript and the README title says Express TypeScript Boilerplate 2025. The project has been renamed at least twice, and the stale references are the kind of thing that sends a first-time contributor to the wrong issue tracker.

The licence is MIT. Dependency updates are handled by Renovate, which the README lists among the features, and the Node version is pinned through a.tool-versions file so that the local toolchain matches the one the container uses.

Editorial conclusion

express-typescript fits a team starting a JSON API that would otherwise spend a week choosing a validation library, a logging library, a test runner and a lint configuration, and that is happy to delete the sample health check and user module and keep the plumbing. It does not fit a team with an existing house style, a GraphQL or server-rendered application, or a service that needs to run on an old Node version. Verify three things before you build on it: that the Express version is the one you want, since the template pins Express 5.2.1 and Express 5 changed error handling behaviour, that the environment variables it validates with Zod match what your deployment sets, because a missing variable stops the process at startup rather than at first use, and that the container build works in your registry, since the Dockerfile installs with --ignore-scripts, which is deliberate for a supply chain reason and can break a dependency that needs a postinstall step.

Frequently asked questions

How do I get the express-typescript template running?

Clone the repository, run pnpm install, copy .env.template to .env and fill in the variables, then run pnpm start:dev for development. For production, set NODE_ENV to production in the .env file, run pnpm build and then pnpm start:prod, which starts node dist/index.js from the build output.

How does the template generate its API documentation?

With Zod and @asteasolutions/zod-to-openapi, so the schemas the requests are validated against are the source of the OpenAPI document, and Swagger UI is served with swagger-ui-express. The generator, the response builders and the router live in src/api-docs/, and there is a test for the API docs router.

What testing and linting does the boilerplate set up?

Vitest with Supertest for HTTP level tests, with test files in __tests__ directories next to the code they cover, and a coverage script using @vitest/coverage-v8. Biome handles linting and formatting together through a single biome check --write script, and the build is tsc and tsup with both esm and cjs output plus generated type declarations.

Which Node version does the template target?

The local toolchain is pinned through a .tool-versions file, described as the newest stable Node.js version, and the Dockerfile builds on node:23.11.1-slim and node:23.11.1-alpine. The package manager is pnpm, pinned at 10.33.0 in the packageManager field and enabled inside the image with corepack.

Is the docker-compose or Dockerfile safe for production use?

The Dockerfile is a three stage build that installs only production dependencies in the final image, runs as the node user and exposes port 8080. One detail to know is that dependencies are installed with --ignore-scripts, which is a supply chain safeguard but means a dependency relying on a postinstall step will not be built correctly without extra handling.

Official sources

  1. edwinhern/express-typescript on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/edwinhern-express-typescript.svg)](https://hysenlabs.com/projects/edwinhern-express-typescript)

Was this useful?